changeset 0:8eeb70d3d1ce

Initial revision
author Guido Berhoerster <guido+slrn@berhoerster.name>
date Sat, 14 Mar 2015 11:43:52 +0100
parents
children 49f639bc9bd9
files README persistent-tags.sl
diffstat 2 files changed, 370 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Sat Mar 14 11:43:52 2015 +0100
@@ -0,0 +1,65 @@
+slrn Persistent Tags Macro
+==========================
+
+The slrn persistent tags macro keeps tags persitent across sessions.
+
+Usage
+-----
+
+The slrn persistent tags macro can be used by including it in the .slrnrc user
+initialization file via the `interpret` command, e.g. provided that the file
+persistent-tags.sl is located in one of the directories specified by the
+macro_directory configuration variable:
+
+    interpret "persistent-tags.sl"
+
+The persistent tags macro can be configured through the following slang
+variables:
+
+PersistentTags->tag_path
+:   Specified the path where tag files are stored.
+
+PersistentTags->autosave
+:   Determines whether tags are saved automatically.
+
+Contact
+-------
+
+Please send any feedback, translations or bug reports via email to
+<guido+weechat@berhoerster.name>.
+
+Bug Reports
+-----------
+
+When sending bug reports, please always mention the exact version of the
+macro with which the issue occurs as well as the version of slrn, slang and
+the operating system you are using and make sure that you provide sufficient
+information to reproduce the issue and include any input, output, any error
+messages and slang stack traces.
+
+License
+-------
+
+Except otherwise noted, all files are Copyright (C) 2009 Guido Berhoerster and
+distributed under the following license terms:
+
+Copyright (C) 2009 Guido Berhoerster <guido+slrn@berhoerster.name>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/persistent-tags.sl	Sat Mar 14 11:43:52 2015 +0100
@@ -0,0 +1,305 @@
+% persistent-tags.sl - keep persistent tags across sessions
+%
+% Copyright (C) 2009 Guido Berhoerster <guido+slrn@berhoerster.name>
+%
+% Permission is hereby granted, free of charge, to any person obtaining
+% a copy of this software and associated documentation files (the
+% "Software"), to deal in the Software without restriction, including
+% without limitation the rights to use, copy, modify, merge, publish,
+% distribute, sublicense, and/or sell copies of the Software, and to
+% permit persons to whom the Software is furnished to do so, subject to
+% the following conditions:
+%
+% The above copyright notice and this permission notice shall be included
+% in all copies or substantial portions of the Software.
+%
+% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+%open_log_file(make_home_filename("slrn-debug.log"));
+%_traceback = 1;
+
+implements("PersistentTags");
+
+private variable rand_next = 1;
+
+% implementation of rand based on an example in IEEE Std 1003.1, 2004 Edition
+static define myrand() {
+    rand_next = rand_next * 1103515245 + 12345;
+    % RAND_MAX is hardcoded to 32767
+    return ((rand_next / 65536U) mod 32768U);
+}
+
+static define mysrand(seed) {
+    rand_next = seed;
+}
+
+private variable URL_SAFE_CHARS =
+        "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-+";
+
+private define urldecode(str)
+{
+    variable decoded_str = ""B;
+    variable char;
+    variable pos = 1;
+    variable opos = 1;
+
+    forever {
+        pos = string_match(str, "%[0-9a-fA-F][0-9a-fA-F]", opos);
+        if (pos == 0)
+            break;
+
+        % add characters between the last match and the current match
+        decoded_str += substr(str, opos, pos - opos);
+        % convert the hex representation of a byte to a byte and append it to
+        % the string
+        char = integer("0x" + substr(str, pos + 1, 2));
+        decoded_str += pack("C", char);
+        opos = pos + 3;
+    }
+    % add remaining charcters
+    decoded_str += substr(str, opos, -1);
+    return typecast(decoded_str, String_Type);
+}
+
+private define urlencode(str)
+{
+    variable char;
+    variable encoded_str = "";
+    variable i;
+    variable j;
+
+    for (i = 0; i < strlen(str); ++i) {
+        char = substr(str, i + 1, 1);
+        ifnot (is_substr(URL_SAFE_CHARS, char)) {
+            for (j = 0; j < strbytelen(char); ++j) {
+                encoded_str += sprintf("%%%02X", char[j]);
+            }
+        } else {
+            encoded_str += sprintf("%s", char);
+        }
+    }
+    return encoded_str;
+}
+
+private variable FILENAME_CHARS =
+        "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+private variable FILENAME_CHARS_LEN = strlen(FILENAME_CHARS);
+
+private define mkstemp(template)
+{
+    variable fd;
+    variable tmp_filename;
+    variable len_template = strlen(@template);
+    variable c;
+
+    if (len_template < 6)
+        return NULL;
+    c = len_template - 6;
+    ifnot (substr(@template, c + 1, len_template) == "XXXXXX")
+        return NULL;
+
+    loop (10000) {
+        tmp_filename = substr(@template, 1, c);
+        loop(6) {
+            tmp_filename += FILENAME_CHARS[[myrand() mod FILENAME_CHARS_LEN]];
+        }
+        fd = open(tmp_filename, O_CREAT|O_EXCL|O_RDWR, 0600);
+        ifnot (fd == NULL) {
+            @template = tmp_filename;
+            break;
+        }
+    }
+
+    return fd;
+}
+
+private define lock_file(filename)
+{
+    variable fd;
+    variable st;
+    variable timeout = qualifier("timeout", 30);
+    variable stale_timeout = qualifier("stale_timeout", 360);
+    variable lockfile = filename + ".lock";
+    variable tmp_lockfile = lockfile + ".XXXXXX";
+    variable time_timeout = _time() + timeout;
+    variable time_stale_timeout = _time() - stale_timeout;
+
+    fd = mkstemp(&tmp_lockfile);
+    if (fd == NULL)
+        return NULL;
+    () = close(fd);
+    try {
+        % attempt to acquire a lock until time_timeout is reached
+        while (_time() < time_timeout) {
+            % try to link lockfile to the previously created temporary file,
+            % link(2) is atomic even on NFSv2 if the lockfile exists link(2)
+            % will fail, this is either detected if EEXIST is returned or the
+            % link count of the temporary file is not 2 in this case try to
+            % remove a stale lockfile, then wait and try again
+            ifnot ((hardlink(tmp_lockfile, lockfile) == 0) ||
+                    (errno == EEXIST))
+                return NULL;
+
+            st = stat_file(tmp_lockfile);
+            if (st == NULL)
+                return NULL;
+            if (st.st_nlink == 2)
+                return lockfile;
+
+            st = stat_file(lockfile);
+            if (st == NULL) {
+                ifnot (errno == ENOENT)
+                    return NULL;
+                else
+                    continue;
+            }
+
+            % remove a stale lockfile after stale_timeout seconds have passed
+            if (st.st_mtime < time_stale_timeout)
+                () = remove(lockfile);
+
+            sleep(2);
+        }
+
+        return NULL;
+    } finally {
+        () = remove(tmp_lockfile);
+    }
+}
+
+static variable config = struct
+{
+    tag_path = ".slrn-tags",
+    autosave = 1
+};
+
+private variable tag_list = Assoc_Type[Null_Type];
+
+private define update_tag_list(ref_tag_list)
+{
+    variable new_tag_list = Assoc_Type[Null_Type];
+    variable needs_update = 0;
+    variable msgid = NULL;
+
+    call("header_bob");
+    % the very first article must be treated specially so it will not be missed
+    % when doing next_tagged_header()
+    while (((msgid == NULL) && (get_header_flags() & HEADER_TAGGED)) ||
+            next_tagged_header()) {
+        msgid = extract_article_header("Message-ID");
+        if (strlen(msgid) == 0)
+            continue;
+        new_tag_list[msgid] = NULL;
+
+        % check if a new element which will go into new_tag_list also exists
+        % in tag_list in order to find any difference between both lists
+        ifnot (needs_update || assoc_key_exists(@ref_tag_list, msgid))
+            needs_update = 1;
+    }
+
+    % if all elements of new_tag_list are also contained in tag_lists
+    % check whether all elements of tag_lists are also contained in
+    % new_tag_lists in order to find any difference between both lists
+    ifnot (needs_update) {
+        foreach msgid(@ref_tag_list) using("keys") {
+            ifnot (assoc_key_exists(new_tag_list, msgid)) {
+                needs_update = 1;
+                break;
+            }
+        }
+    }
+
+    % replace tag_list with new_tag_list
+    @ref_tag_list = new_tag_list;
+    return needs_update;
+}
+
+static define save_tags()
+{
+    variable line, fp;
+    variable dir = path_concat(make_home_filename(config.tag_path),
+            urlencode(server_name()));
+    variable filename = path_concat(dir, urlencode(current_newsgroup()));
+    variable lockfile;
+
+    ifnot (update_tag_list(&tag_list))
+        return;
+
+    if (mkdir(dir) == -1) {
+        ifnot (errno == EEXIST)
+            throw IOError, "mkdir $dir failed: "$ + errno_string(errno);
+    }
+
+    lockfile = lock_file(filename);
+    if (lockfile == NULL)
+        throw IOError, "failed to lock $filename"$;
+
+    try {
+        % if tag_list is empty remove the file
+        if (length(tag_list) == 0) {
+            () = remove(filename);
+            return;
+        }
+
+        fp = fopen(filename, "w");
+        if (fp == NULL)
+            throw OpenError, "opening $filename failed: "$ +
+                    errno_string(errno);
+        foreach line(tag_list) using("keys") {
+            if (fputs(line + "\n", fp) == -1)
+                throw WriteError, "writing to $filename failed: "$ +
+                        errno_string(errno);
+        }
+        () = fclose(fp);
+    } finally {
+        () = remove(lockfile);
+    }
+    return;
+}
+
+static define load_tags()
+{
+    variable fp, buf, msgid, pos;
+    variable dir = path_concat(make_home_filename(config.tag_path),
+            urlencode(server_name()));
+    variable filename = path_concat(dir, urlencode(current_newsgroup()));
+
+    % re-ininitalize tag_list
+    tag_list = Assoc_Type[Null_Type];
+
+    fp = fopen(filename, "r");
+    if (fp == NULL) {
+        if (errno == ENOENT)
+            return;
+        throw OpenError, "opening $filename failed: "$ + errno_string(errno);
+    }
+
+    % save position to restore after applying tags
+    pos = extract_article_header("Message-ID");
+    while (fgets(&buf, fp) != -1) {
+        msgid = strtrim(buf);
+        if (strlen(msgid)) {
+            tag_list[msgid] = NULL;
+            if (locate_header_by_msgid(msgid, 1))
+                set_header_flags(get_header_flags() | HEADER_TAGGED);
+        }
+    }
+    () = fclose(fp);
+    () = locate_header_by_msgid(pos, 0);
+
+    return;
+}
+
+mysrand(_time() * getpid());
+
+if (config.autosave)
+{
+    () = register_hook("article_mode_hook", "PersistentTags->load_tags");
+    () = register_hook("article_mode_quit_hook", "PersistentTags->save_tags");
+}