insult: new module

insult: new module

Former-commit-id: 396e1395a72969475da32126b618c13c450675b0 [formerly 4fbaec6d39b2528f0d80ce8e84793d4f5aad5865]
Former-commit-id: 7a824f36bdb1f4ddbc1e205d3a6c22fca43e9826

-- a port of https://github.com/LizardGamer/tumblr-argument-generator/blob/develop/app/assets/js/main.js
-- original by Lokaltog
-- github pls no delete

intro = {
  'burn in hell',
  'check your privilege',
  'fuck you',
  'fuck off',
  'please die',
  'rot in hell',
  'screw you',
  'shut the fuck up',
  'shut up',
  'kill yourself',
  'drop dead'
}
description = {
  'deluded',
  'fucking',
  'god damn',
  'judgemental',
  'worthless'
}
marginalized = {
  {
    'activist',
    'agender',
    'appearance',
    'asian',
    'attractive',
    'bi',
    'bigender',
    'black',
    'celestial',
    'chubby',
    'closet',
    'color',
    'curvy',
    'dandy',
    'deathfat',
    'demi',
    'differently abled',
    'disabled',
    'diversity',
    'dysphoria',
    'ethnic',
    'ethnicity',
    'fat love',
    'fat',
    'fatist',
    'fatty',
    'female',
    'feminist',
    'genderfuck',
    'genderless',
    'hair',
    'height',
    'indigenous',
    'intersectionality',
    'invisible',
    'kin',
    'lesbianism',
    'little person',
    'marginalized',
    'minority',
    'multigender',
    'non-gender',
    'non-white',
    'obesity',
    'otherkin',
    'pansexual',
    'polygender',
    'privilege',
    'prosthetic',
    'queer',
    'radfem',
    'skinny',
    'smallfat',
    'stretchmark',
    'thin',
    'third-gender',
    'trans*',
    'transfat',
    'transgender',
    'transman',
    'transwoman',
    'trigger',
    'two-spirit',
    'womyn',
    'poc',
    'woc'
  },
  {
    'chauvinistic',
    'misogynistic',
    'nphobic',
    'oppressive',
    'phobic',
    'shaming',
    'denying',
    'discriminating',
    'hypersexualizing',
    'intolerant',
    'racist',
    'sexualizing'
  }
}
privileged = {
  {
    'able-bodied',
    'appearance',
    'attractive',
    'binary',
    'bi',
    'cis',
    'cisgender',
    'cishet',
    'hetero',
    'male',
    'smallfat',
    'thin',
    'white'
  },
  {
    'ableist',
    'classist',
    'normative',
    'overprivileged',
    'patriarch',
    'sexist',
    'privileged'
  }
}
finisher = {
  'asshole',
  'bigot',
  'oppressor',
  'piece of shit',
  'rapist',
  'scum',
  'shitlord',
  'subhuman',
  'misogynist',
  'nazi'
}

getRandomItem = (array) ->
  array[math.random(1, #array)]

generateTerm = ->
  getRandomItem({
    'a',
    'bi',
    'dandy',
    'demi',
    'gender',
    'multi',
    'pan',
    'poly'
  }) .. getRandomItem({
    'amorous',
    'femme',
    'fluid',
    'queer',
    'romantic',
    'sexual',
  })

generateArgument = ->
  buf = {
    getRandomItem(intro),
    ', you ',
    getRandomItem(description),
    ' ',
    getRandomItem({
      generateTerm(),
      getRandomItem(marginalized[1])
    }),
    '-',
    getRandomItem(marginalized[2]),
    ', ',
    getRandomItem(privileged[1]),
    '-',
    getRandomItem(privileged[2]),
    ' ',
    getRandomItem(finisher),
    '.',
  }
  table.concat(buf)

PRIVMSG:
  '^%pinsult$': (source, destination) =>
    @Msg 'privmsg', destination, source, generateArgument()
  '^%pinsult (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, arg..": "..generateArgument()

error: pathspec 'modules/insult.moon' did not match any file(s) known to git


redditjoke: New module, get jokes from /r/jokes

Former-commit-id: fb8de8836fb74bd2e926594b733d078a22cec535 [formerly 91e8d019018f6ea64b893b355767b2537479138f]
Former-commit-id: 5e55592a82107be65b5f401e1eb04bf1f2abc280

json = require'json'
simplehttp = require'simplehttp'
math = require'math'

PRIVMSG:
  '^%pjoke': (source, destination, arg) =>
    simplehttp 'http://www.reddit.com/r/jokes.json', (data) ->
      data = json.decode data
      children = data.data.children
      joke = children[math.random 1, #children]
      title = joke.data.title
      joketext = title .. ' ' .. joke.data.selftext
      @Msg 'privmsg', destination, source, joketext

error: pathspec 'modules/redditjoke.moon' did not match any file(s) known to git


fortune: new silly module

fortune: new silly module

--fileno = (require "posix").fileno -- could be org.conman.fsys instead
--cqueues = require "cqueues"
--queue = cqueues.new()

PRIVMSG:
  '^%pfortune$': (s, d) =>
    f = assert(io.popen("/usr/games/fortune","r"))
    data = f\read("*a") -- this isn't necessarily correct; if a whole line isn't ready to read it will block
    if data
      say data
    f\close!
    -- nonblocking
    --
--    cnt = 0
--    notdone = true
--    fortune = ''
--    queue\wrap ->
--        f = assert(io.popen("/usr/games/fortune","r"))
--        pollable = {
--            pollfd: fileno(f)
--            events: "r"
--        }
--        while cqueues.poll(pollable) -- yield the current thread until we have data
--          data = f\read("*l") -- this isn't necessarily correct; if a whole line isn't ready to read it will block
--          if data == nil then
--              -- f:read returns nil on EOF
--              f\close()
--              break
--          else
--            fortune = fortune .. data
--        notdone = false
--        say fortune
--    queue\wrap ->
--        while notdone
--            cnt = cnt + 1
--            cqueues.poll() -- yield the current thread
--    queue\loop!

error: pathspec 'modules/fortune.moon' did not match any file(s) known to git


Add util tests

Add util tests

Testing for the `util` class.

package.path = '../?.lua;#{package.path}'
package.loaded['lapis.html'] = {}

describe 'utilsities module', ->
    setup ->
        export utils = require 'utils'

    teardown ->
        utils = nil

    describe 'split function', ->
        it 'fails with invalid separators', ->
            assert.falsy utils.split 'this is a string', nil
            assert.falsy utils.split 'this is a string', ''

        it 'fails with a nil string', ->
            assert.falsy utils.split nil, 'separator'

        it 'can split a basic string', ->
            r, n = utils.split 'this is a string', ' '
            assert.equals 4, n
            assert.are.same { 'this', 'is', 'a', 'string' }, r

        it 'handles a separator at the end of the string', ->
            r, n = utils.split 'sep end ', ' '
            assert.equals 3, n
            assert.are.same { 'sep', 'end', '' }, r

        it 'handles a separator at the beginning of the string', ->
            r, n = utils.split ' sep start', ' '
            assert.equals 3, n
            assert.are.same { '', 'sep', 'start' }, r

        it 'handles successive separators', ->
            r, n = utils.split 'sep  successive', ' '
            assert.equals 3, n
            assert.are.same { 'sep', '', 'successive' }, r

        it 'can split using multi-character separators', ->
            r, n = utils.split 'multicharSEPisSEPhere', 'SEP'
            assert.equals 3, n
            assert.are.same { 'multichar', 'is', 'here' }, r

        it 'handles a multichar separator at the end of the string', ->
            r, n = utils.split 'multisep{sep}sep{sep}', '{sep}'
            assert.equals 3, n
            assert.are.same { 'multisep', 'sep', '' }, r

        it 'handles a multichar separator at the beginning of the string', ->
            r, n = utils.split '{sep}multisep{sep}sep', '{sep}'
            assert.equals 3, n
            assert.are.same { '', 'multisep', 'sep' }, r

        it 'handles successive multichar separators', ->
            r, n = utils.split 'multisep{sep}{sep}successive', '{sep}'
            assert.equals 3, n
            assert.are.same { 'multisep', '', 'successive' }, r

        it 'handles a string consisting only of separators', ->
            r, n = utils.split ',,,', ','
            assert.equals 4, n
            assert.are.same { '', '', '', '' }, r

    describe 'json_requested function', ->
        calljr = (hdr) ->
            utils.json_requested { res: { req: { headers: { accept: hdr } } } }

        it 'handles nil requests', ->
            assert.falsy utils.json_requested nil

        it 'handles invalid requests', ->
            assert.falsy utils.json_requested { }
            assert.falsy utils.json_requested { res: { } }
            assert.falsy utils.json_requested { res: { req: { } } }

        it 'handles when the Accepts header is not present', ->
            assert.falsy utils.json_requested { res: { req: { headers: { } } } }

        testcases =
            ['application/json']: true
            ['application/json, */*']: true
            ['application/json; q=0.05']: true
            ['application/json, */*; q =0.05']: true
            ['application/json, text/html']: true
            ['application/json, text/html; q=0.05']: true
            ['application/json  ']: true
            ['application/json ; q=0.05']: true
            ['application/json ,text/html; q=0.05']: true
            ['text/html, application/json; q=0.05']: false
            ['text/html, application/json']: false

        for h, r in pairs testcases
            it "works with `#{h}`", ->
                assert.equals r, calljr h

error: pathspec 'spec/utils_spec.moon' did not match any file(s) known to git


github: add module for github web hooks

github: add module for github web hooks

util = require 'util'

ivar2.webserver.regUrl '/github/(.*)', (req, res) ->
  destination = req.url\match('/github/(.+)/?$')
  destination = destination\gsub '%%23', '#'
  json = util.json.decode(req.body)

  repo = json.repository.full_name
  branch = json.ref\gsub 'refs/heads/', ''

  for c in *json.commits
    ivar2\Msg 'privmsg', destination, nil, "#{repo}: #{branch} #{c.author.username} #{c.message}"

  res\set_status(200)
  res\set_header('Content-Type', 'text/plain')
  res\set_body('ok')
  res\send()

return {}

error: pathspec 'modules/github.moon' did not match any file(s) known to git


youtube: new module. thanks to @einarjh for idea

Former-commit-id: 30fa6c935dc77478f2f94ba0cba2c7c0ea1d16f1 [formerly d8e80289747ec9745878c163116171f8c131f332]
Former-commit-id: 71e4d61699546b0b4bdcf06940a3139d0ccbed93

-- Module for announcing new videos to a youtube channel
-- Requires a developer API key set in configuration
{:simplehttp, :json, :urlEncode, :bold} = require'util'

moduleName = 'youtube'
key = moduleName
store = ivar2.persist

getItems = (channel, cb) ->
  url = "https://www.googleapis.com/youtube/v3/channels?part=contentDetails&forUsername=%s&key=%s"
  simplehttp url\format(urlEncode(channel), ivar2.config.youtubeAPIKey), (data) ->
    js = json.decode(data)
    if not (js and js.items and js.items[1] and js.items[1].contentDetails)
      ivar2\Log 'info', 'youtube: No items for youtube channel '..tostring(channel)
      return
    playlistId = js.items[1].contentDetails.relatedPlaylists.uploads
    if playlistId
      url = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=%s&key=%s"
      simplehttp url\format(playlistId, ivar2.config.youtubeAPIKey), (pldata) ->
        pljs = json.decode(pldata)
        cb(pljs.items)

getLatest = (source, destination, channel) =>
  getItems channel, (items) ->
    for item in *items
      if item.kind == "youtube#playlistItem"
        s = item.snippet
        videoId = s.resourceId.videoId
        say "[#{bold s.channelTitle}] http://youtu.be/#{videoId} #{s.title}"
        return

checkChannels = ->
  for c,_ in pairs(ivar2.channels)
    channelKey = key..':'..c
    channels = store[channelKey] or {}
    for name, channel in pairs(channels)
      lastKey = channelKey .. ':' .. name .. ':last'
      last = store[lastKey] or ''
      getItems name, (items) ->
        for item in *items
          if item.kind == "youtube#playlistItem"
            s = item.snippet
            videoId = s.resourceId.videoId
            -- Check if we already announced
            if videoId == last
              return
            -- Store first video in playlist as a marker
            if s.position == 0
              store[lastKey] = videoId
            ivar2\Msg 'privmsg', channel.channel, nil, "[#{bold s.channelTitle}] http://youtu.be/#{videoId} — #{s.title}"
            -- If we're newly subscribed, only tell once
            if last == ''
              return

subscribe = (source, destination, name) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  channels[name] = {channel:destination, name:name}
  store[channelKey] = channels
  reply "Ok. Subscribed to #{bold name}"
  checkChannels()

unsubscribe = (source, destination, name) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  unless channels[name]
    reply "Wasn't subscribed. But, sure."
  else
    channels[name] = nil
    store[channelKey] = channels
    reply "Ok. Stopped caring about #{bold name}"

list = (source, destination) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  out = {}
  for name, game in pairs(channels)
      out[#out+1] = name
  say "Subscribed to: #{table.concat(out, ', ')}"

-- Start the subscribe poller
ivar2\Timer('youtube', 300, 300, checkChannels)

PRIVMSG:
  '^%pyoutube latest (.*)': getLatest
  '^%pyoutube subscribe (.*)': subscribe
  '^%pyoutube unsubscribe (.*)': unsubscribe
  '^%pyoutube list': list

error: pathspec 'modules/youtube.moon' did not match any file(s) known to git


motivate: new module (also see motivate.im)

PickOne = (l) ->
 l[math.random #l]

phrases = {
  "You're doing good work, %s"
  "%s, you are an inspiration to all of us"
  "Kudos, %s, kudos."
  "Keep up the great work, %s"
  "You're awesome, %s"
  "Keep on truckin', %s"
  "High five, %s, that's awesome"
  "%s: rock on!"
}

PRIVMSG:
  '^%pmotivate (.*)$': (s, d, n) => say PickOne(phrases), n
  '^%^5 (.*)$': => reply '⁵!!'
  '^%^5$': => say '⁵!!'

error: pathspec 'modules/motivate.moon' did not match any file(s) known to git


naustver: rename and rework to support multiple

Former-commit-id: 3444cf3bbdcd29d746d7de80fbd4900c98300fb9 [formerly d6f663223037458729eb6ddcd9c016aaf0fc6f94]
Former-commit-id: 9288ebb7504268e922f647fb23f18230c162aadd

{:simplehttp, :json} = require'util'

windSpeedToName = (ws) ->
  windtext = ''
  unless ws
    return windtext
  if ws > 32
    windtext = 'orkan!'
  elseif ws > 28.5
    windtext = 'sterk storm'
  elseif ws > 24.5
    windtext = 'full storm'
  elseif ws > 20.8
    windtext = 'liten storm'
  elseif ws > 17.2
    windtext = 'sterk kuling'
  elseif ws > 13.9
    windtext = 'stiv kuling'
  elseif ws > 10.8
    windtext = 'liten kuling'
  elseif ws > 8
    windtext = 'frisk bris'
  elseif ws > 5.5
    windtext = 'laber bris'
  elseif ws > 3.4
    windtext = 'lett bris'
  elseif ws > 1.5
    windtext = 'svak vind'
  elseif ws > 0.3
    windtext = 'flau vind'
  else
    windtext = 'vindstille'
  return windtext

handleWeather = (source, destination, url) =>
    simplehttp url, (data) ->
      result = json.decode data
      if result
        result = result[1]
        temp = result.outtemp
        ws = result.windspeed / 3.6 -- correct unit
        rain = result.dayrain
        windtext = windSpeedToName(ws)
        wgs = windSpeedToName(result.windgust/3.6)
        if wgs ~= windtext
          wgs = ", #{wgs} i kasta"
        else
          wgs = ''

        time = result.datetime\sub(12, 16)

        msg = string.format '\002%.1f\002 °C, \002%.1f\002 m/s (%s%s), \002%.1f\002 mm nedbør (%s)', temp, ws, windtext, wgs, rain, time
        @Privmsg destination, msg

PRIVMSG:
  '^%pnaustvêr$': (source, destination) =>
    url = 'http://yr.hveem.no/api/now'
    handleWeather(@, source, destination, url)
  '^%pvêr$': (source, destination, input) =>
    url = 'http://yr.teigen.be/api/now'
    handleWeather(@, source, destination, url)

error: pathspec 'modules/amatyr.moon' did not match any file(s) known to git


aunesand: new module

aunesand: new module

Former-commit-id: df31216c026365f4a21445ecde0de12c3c8e0f66 [formerly b4637bad082ae44103f889a63330d23acd1930f1]
Former-commit-id: 1c35c422f88dc42497fe677ce87fc10849f4b1cf

html2unicode = require'html'

hex_to_char = (x) ->
  string.char(tonumber(x, 16))

unescape = (url) ->
  url\gsub("%%(%x%x)", hex_to_char)

PRIVMSG:
  '^%paunesand': (source, destination) =>
    rnd = math.random(1,3000)
    ivar2.util.simplehttp 'http://p3.no/uv/aune-sands-poesigenerator/?cachebuster='..rnd, (html) ->
      match = html\match[[<a title="Del diktet på Facebook" href="(.*)"]]
      if match
        poem = match\match[[facebookdescription=(.-)-]]
        poem = poem\gsub '+', ' '
        poem = unescape(poem)
        if poem
          say html2unicode(poem)

error: pathspec 'modules/aunesand.moon' did not match any file(s) known to git


thank-you: new module, don't mention it

thank-you: new module, don't mention it

Former-commit-id: a66d0bc9ba56ce3ff7ab553ef26c60dfe6f8bf3a [formerly fce345cf7b7396126ccfaf5cc2ee586b88c98e4a]
Former-commit-id: ef312d5572e4ae629e639db92c138deb54a39aa7

-- Ported from hubot which sites this source:
-- http://www.macmillandictionary.com/thesaurus-category/british/Ways-of-accepting-someone-s-thanks

response = {
  "you're welcome",
  "no problem",
  "not at all",
  "don’t mention it",
  "it’s no bother",
  "it’s my pleasure",
  "my pleasure",
  "it’s all right",
  "it’s nothing",
  "think nothing of it",
  "sure",
  "sure thing"
}

answer = (s, d, a) =>
  reply response[math.random(#response)]

PRIVMSG:
  "#{ivar2.config.nick}.-takk": answer
  "#{ivar2.config.nick}.-thank": answer
  "takk.-#{ivar2.config.nick}": answer
  "thanks.-#{ivar2.config.nick}": answer

error: pathspec 'modules/thank-you.moon' did not match any file(s) known to git


Add a screen command to force a new session

error: pathspec 'terminal/filesystem/root/bin/screen.moon' did not match any file(s) known to git


Add keyboard wrappers

Add keyboard wrappers

import java from sauce3

Gdx = java.require "com.badlogic.gdx.Gdx"
Peripheral = java.require "com.badlogic.gdx.Input$Peripheral"
Constants = require "sauce3.wrappers"

is_available = ->
  is_visible! or Gdx.input\isPeripheralAvailable Peripheral.HardwareKeyboard

is_down = (...) ->
  args = table.pack ...

  found = false

  for i = 1, args.n
    k = Constants.keys[args[i]]
    found = found or Gdx.input\isKeyPressed k
  found

is_visible = ->
  Gdx.input\isPeripheralAvailable Peripheral.OnscreenKeyboard

set_visible = (v) ->
  Gdx.input\setOnscreenKeyboardVisible v

{
  :is_down
  :is_visible
  :is_available
  :set_visible
}

error: pathspec 'src/sauce3/wrappers/keyboard.moon' did not match any file(s) known to git


Alot: new module

Alot: new module

alot = (source, destination) =>
  @Msg 'privmsg', destination, source, 'http://hyperboleandahalf.blogspot.no/2010/04/alot-is-better-than-you-at-everything.html'
PRIVMSG:
  ' [aA][lL][oO][tT] ': alot
  '^[aA][lL][oO][tT] ': alot
  ' [aA][lL][oO][tT]$': alot
  '[aA][lL][oO][tT]%p': alot
  '%p[aA][lL][oO][tT]%p': alot
  '^[aA][lL][oO][tT]$': alot

error: pathspec 'modules/alot.moon' did not match any file(s) known to git


omdb: new movie lookup module, replace imdb and rt

Former-commit-id: 5aa393ccdb0b8ff9630f6b67cecc21a344e61d8f [formerly 611c3b9d4105cd9a1ab6a9abb86dd181125b9375]
Former-commit-id: 574ccc084380d772dfaeb2b9ee29f3c500b08714

util = require 'util'

rtcolor = (score) ->
  if not tonumber(score) then return score
  if tonumber(score) > 50
    return util.red(string.format("%s%%", score))
  else
    return util.green(string.format("%s%%", score))

metacolor = (score) ->
  if not tonumber(score) then return score
  if tonumber(score) >= 60
    return util.green(string.format("%s%%", score))
  elseif tonumber(score) => 40
    return util.yellow(string.format("%s%%", score))
  else
    return util.red(string.format("%s%%", score))

omdbfetch = (arg, cb) ->
  util.simplehttp "http://www.omdbapi.com/?t=#{util.urlEncode arg}&y=&plot=short&r=json&tomatoes=true", (data) ->
    js = util.json.decode(data)
    if js and not js.Error
      cb(js)
    else
      say('(%d) %s', js.code, js.Error)

omdb = (source, destination, arg) =>
  omdbfetch arg, (js) ->
    say "[#{util.bold js.Title}] (#{js.Year}) #{js.Genre} Metacritic: [#{metacolor js.Metascore}] RT: [#{rtcolor js.tomatoMeter} / #{rtcolor js.tomatoRotten}] IMDB: [#{js.imdbRating}] #{js.Plot}"

plot = (source, destination, arg) =>
  omdbfetch arg, (js) ->
    say "[#{util.bold js.Title}] (#{js.Year}) #{js.Plot}"

PRIVMSG:
  '^%pomdb (.+)': omdb
  '^%pimdb (.+)': omdb
  '^%prt (.+)': omdb
  '^%pmovie (.+)': omdb
  '^%pplot (.+)': plot

error: pathspec 'modules/omdb.moon' did not match any file(s) known to git


utf8: new module

utf8: new module

Former-commit-id: 01505d27d2023af77663268536247687de6d69ff [formerly 50ada6923d3c7c445cf5bef8df687a819f319abf]
Former-commit-id: 7129da2791f35e08900da804bc082d0111ba6071

html2unicode = require'html'

replace = (offset, arg) ->
    s = arg or ''
    t = {}
    for i = 1, #s
      bc = string.byte(s, i, i)
      if bc == 32
        t[#t + 1] = '\227\128\128'
      elseif bc < 0x80 then
        t[#t + 1] = html2unicode("&#" .. (offset + bc) .. ";")
      else
        t[#t + 1] = s\sub(i, i)

    table.concat(t, "")

wide = (source, destination, arg) =>
  @Msg 'privmsg', destination, source, replace(0xFEE0, arg)

blackletter = (source, destination, arg) =>
  @Msg 'privmsg', destination, source, replace(0x1D4A3, arg)

PRIVMSG:
  '^%pwide (.+)$': wide
  '^%pblackletter (.+)$': blackletter

error: pathspec 'modules/utf8.moon' did not match any file(s) known to git


imageupload: new module for easy media sharing to IRC

{:simplehttp, :json, :urlEncode} = require'util'
html2unicode = require 'html'
nixio = require'nixio'
ltn12 = require'ltn12'

urlbase = '/image/'

safe = (fn) ->
  f, ext = fn\match'^(.*)%.(.-)$'
  f = f\gsub '[^%w%-]', ''
  return f..'.'..ext

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) ->
  send = (body, code, content_type) ->
    if not code then code = 200
    if not content_type then content_type = 'text/html'
    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', #body
    res\set_body body
    res\send!
    res.on_error = (res, req, err) ->
      ivar2\Log 'error', 'imageupload: error: %s', err

  file = req.url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = nixio.fs.stat(fn).size
    fd = ltn12.source.file(io.open fn, 'rb')
    --body = fd\read '*a'
    --fd\close!
    content_type = 'image/jpeg'
    if file\match '.png'
      content_type = 'image/png'
    if file\match '.svg'
      content_type = 'image/svg'
    if file\match '.mp4'
      content_type = 'video/mp4'
    if file\match '.mp3'
      content_type = 'audio/mp3'

    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', size
    res\set_body fd
    res\send!
    ivar2\Log 'error', 'imageupload: serving: %s', fn

    return

  channel = req.url\match('channel=(.+)')
  unless channel
    send 'Invalid channel', 404
    return

  channel = html2unicode channel
  unescaped_channel = channel\gsub '%%23', '#'

  html = [[
  <!DOCTYPE html>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="IRCSNAP ]]..unescaped_channel..[[">
  <meta name="theme-color" content="#8f4099">

  <title>IRCSNAP</title>

Share to IRC app - ]]..unescaped_channel..[[ edition

Fill in your nick/name and optional text and then click on one of the two yellow buttons to attach image. It will appear instantly on IRC!</p
    <form>

        <input id="sender" type="text" required>

        Your name

        <input id="text" type="text" required>

        Attach text

    </form>

Uploading. Percent complete: 0 %

      📷 Snap picture! <input type="file" accept="image/*" id="capturei" capture="camera" style="visibility:hidden;">
      📂 Browse gallery! <input type="file" accept="image/*" id="capturef" style="visibility:hidden;">

        What is this?

Share to IRC is a simple web app for sharing media directly from your device to an IRC channel.

Click Tools -> Add to homescreen to create a convenient shortcut for easy and fast access to sharing

  ]]
  if req.method == 'POST'
    fn = req.headers['X-Filename']
    sender = req.headers['X-Sender'] or ''
    text = req.headers['X-Text'] or ''
    file = req.body
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        fd = nixio.open "cache/images/#{realfn}", 'w+', 0400
        written = fd\write file
        fd\sync!
        fd\close!
        if sender ~= ''
          sender = "<#{sender}> "
        msg = "[IRCSNAP] #{sender}#{text\sub(1, 100)} http://irc.lart.no:#{ivar2.config.webserverport}#{urlbase}file/#{realfn}"
        ivar2\Privmsg unescaped_channel, msg
      ok, err = pcall(save)
      unless ok
        ivar2\Log 'error', "imageupload: Error during saving upload: %s", err
    else
      html = 'Not OK'

  send html

-- Attempt to create the cache folder.
nixio.fs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "http://irc.lart.no:#{ivar2.config.webserverport}#{urlbase}?channel=#{channel}"

error: pathspec 'modules/imageupload.moon' did not match any file(s) known to git


autoop: new module, autoop owners and chan owners

Former-commit-id: 705d2d34f6bdc0f209ae493593bf631924113a01 [formerly 233e44a70be7aee37f451ca9a569a88310bbd3e1]
Former-commit-id: 1cbdd697865e1d1a19313c98564639d289c2d229

verifyOwner = (src) ->
  for _,mask in pairs(ivar2.config.owners)
    if src.mask\match(mask)
      return true

verifyChannelOwner = (src, destination) ->
  channel = ivar2.config.channels[destination]

  if type(channel) == 'table' and type(channel.owners) == 'table'
    for _,mask in pairs(channel.owners)
      if src.mask\match(mask)
        return true

JOIN: {
  (source, destination, arg) =>
    if verifyOwner source
      @Log 'info', 'Automatically OPing owner'
      @Mode destination, "+o #{source.nick}"
    elseif verifyChannelOwner source, destination
      @Log 'info', 'Automatically OPing channel owner'
      @Mode destination, "+o #{source.nick}"

}

error: pathspec 'modules/autoop.moon' did not match any file(s) known to git


github: add module for github web hooks

github: add module for github web hooks

Former-commit-id: e93f2336e1dd058b2da1d924b26499d1d9ad4365 [formerly 01319c9bfe0eede2a1aaf34d7498c2ee587cc269]
Former-commit-id: eb6dc500007b67d5d4f721c76e9ce4fb6f14421f


charstat: silly module for tracking char winners

Former-commit-id: 08de844b478cd4ace25d37220d80cfba4e6b4381
Former-commit-id: 57d92574f4807c9bf9e6b349d9b073079220fdc5

store = ivar2.persist

key = (c) ->
  "charstat:#{c}"

PRIVMSG:
  '^[a-zA-Z]$': (source, destination, c) =>
    c = c\lower!
    cur = store[key c]
    cur = tonumber(cur) or 0
    cur = cur + 1
    store[key c] = cur
  '^%pcharwin$': (source, destination) =>
    chars = 'abcdefghijklmnopqrstuvxyz'
    out = {}
    i = 1
    for c in chars\gmatch '.'
      cur = store[key c]
      cur = tonumber(cur) or 0
      out[i] = {:c,:cur}
      i = i + 1

    table.sort out, (a, b) ->
      if a.cur > b.cur
        return true
      return false

    out_s = {}
    for i, v in ipairs(out)
      if i > 5
        break
      if v.cur > 0
        out_s[#out_s+1] = "#{ivar2.util.bold v.c}: #{v.cur}"

    say "Winners: #{table.concat(out_s, ' ')}"

error: pathspec 'modules/charstat.moon' did not match any file(s) known to git


translate: new module, (ab)use google translate

-- google translate
json = require'json'
simplehttp = require'simplehttp'

languages =
  "af": "Afrikaans",
  "sq": "Albanian",
  "ar": "Arabic",
  "az": "Azerbaijani",
  "eu": "Basque",
  "bn": "Bengali",
  "be": "Belarusian",
  "bg": "Bulgarian",
  "ca": "Catalan",
  "zh-CN": "Simplified Chinese",
  "zh-TW": "Traditional Chinese",
  "hr": "Croatian",
  "cs": "Czech",
  "da": "Danish",
  "nl": "Dutch",
  "en": "English",
  "eo": "Esperanto",
  "et": "Estonian",
  "tl": "Filipino",
  "fi": "Finnish",
  "fr": "French",
  "gl": "Galician",
  "ka": "Georgian",
  "de": "German",
  "el": "Greek",
  "gu": "Gujarati",
  "ht": "Haitian Creole",
  "iw": "Hebrew",
  "hi": "Hindi",
  "hu": "Hungarian",
  "is": "Icelandic",
  "id": "Indonesian",
  "ga": "Irish",
  "it": "Italian",
  "ja": "Japanese",
  "kn": "Kannada",
  "ko": "Korean",
  "la": "Latin",
  "lv": "Latvian",
  "lt": "Lithuanian",
  "mk": "Macedonian",
  "ms": "Malay",
  "mt": "Maltese",
  "no": "Norwegian",
  "fa": "Persian",
  "pl": "Polish",
  "pt": "Portuguese",
  "ro": "Romanian",
  "ru": "Russian",
  "sr": "Serbian",
  "sk": "Slovak",
  "sl": "Slovenian",
  "es": "Spanish",
  "sw": "Swahili",
  "sv": "Swedish",
  "ta": "Tamil",
  "te": "Telugu",
  "th": "Thai",
  "tr": "Turkish",
  "uk": "Ukrainian",
  "ur": "Urdu",
  "vi": "Vietnamese",
  "cy": "Welsh",
  "yi": "Yiddish"

getCode = (language) ->
  for code, lang in pairs languages
    if lang\lower! == language\lower!
      return code
    else if language\lower! == code\lower!
      return code

urlEncode = (str, space) ->
  space = space or '+'
  str = tostring(str)

  str = str\gsub '([^%w ])', (c) ->
    string.format  "%%%02X", string.byte(c)
  return str\gsub(' ', space)

buildQueryStr = (tbl) ->
  out = {}
  for k,v in pairs tbl
    table.insert out, k..'='..urlEncode(v)
  table.concat out, '&'

translate = (source, destination, origin, target, term) =>
  origin = getCode(origin) or 'auto'
  target = getCode(target) or 'en'
  args =
    client: 'p'
    hl: 'en'
    multires: 1
    sc: 1
    sl: origin
    ssel: 0
    tl: target
    tsel: 0
    uptl: "en"
    text: term

  simplehttp "https://translate.google.com/translate_a/t?" .. buildQueryStr(args), (data) ->
    if #data > 4
      parsed = json.decode data
      language = parsed.src
      out = table.concat [k.trans for k in *parsed.sentences]
      if #out > 0
        @Msg 'privmsg', destination, source, "\002#{languages[target]}\002: #{out}"

PRIVMSG:
  '^%ptranslate ([%w%-]+) ([%w%-]+) (.*)$': translate

error: pathspec 'modules/translate.moon' did not match any file(s) known to git


fork me ribbons

fork me ribbons

html = require "lapis.html"

class MainLayout extends html.Widget
  @include require "views.mixin.favicon"
  @include require "lazuli.modules.user_management.views.mixin_menu"
  content: =>
    html_5 ->
      head ->
        meta charset:"utf-8"
        meta name:"viewport", content:"width=device-width, initial-scale=1"
        link rel:"stylesheet", href:"http://yui.yahooapis.com/pure/0.6.0/pure-min.css"
        link rel:"stylesheet", href:"http://yui.yahooapis.com/pure/0.6.0/grids-responsive-min.css"
        link rel:"stylesheet", href:"/static/css/main_layout.css"
        link rel:"stylesheet", href:"//cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.1.1/gh-fork-ribbon.min.css"
        title @title or (@has_content_for("title") and @content_for("title")) or "Komischfarben"
        @mixFavicon!
      body ->
        div class:"github-fork-ribbon-wrapper right",->
          div class:"github-fork-ribbon",->
            a href:"https://github.com/nonchip/komischfarben2", "Fork me on GitHub"
        div id:"layout",->
          a href:"#menu",id:"menuLink",class:"menu-link",->
            span ""
          div id:"menu",->
            div class:"pure-menu pure-menu-open",->
              a class:"pure-menu-heading", href:"/", "Komischfarben"
              @userManagementMixinMenu{ul:"pure-menu-list",li:"pure-menu-item",a:"pure-menu-link", active_li:"pure-menu-selected"}
              ul class:"pure-menu-list", ->
                --li class:"pure-menu-item",->
                --  a class:"pure-menu-link",href:"https://github.com/nonchip/komischfarben2",style:"color:#555",target:"_blank","Github"
                li class:"pure-menu-item",->
                  a class:"pure-menu-link",href:"https://github.com/nonchip/komischfarben2/issues",style:"color:#555",target:"_blank","Report issues"
          div id:"main",->
            if true or @has_content_for "header" then
              div class:"header",->
                tt "BETA"
                @content_for "header"
            div class:"content",->
              @content_for "inner"
        script src:"/static/js/main_layout.js"

html = require "lapis.html"

class MainLayout extends html.Widget
  @include require "views.mixin.favicon"
  @include require "lazuli.modules.user_management.views.mixin_menu"
  content: =>
    html_5 ->
      head ->
        meta charset:"utf-8"
        meta name:"viewport", content:"width=device-width, initial-scale=1"
        link rel:"stylesheet", href:"http://yui.yahooapis.com/pure/0.6.0/pure-min.css"
        link rel:"stylesheet", href:"http://yui.yahooapis.com/pure/0.6.0/grids-responsive-min.css"
        link rel:"stylesheet", href:"/static/css/main_layout.css"
        title @title or (@has_content_for("title") and @content_for("title")) or "Komischfarben"
        @mixFavicon!
      body ->
        div id:"layout",->
          a href:"#menu",id:"menuLink",class:"menu-link",->
            span ""
          div id:"menu",->
            div class:"pure-menu pure-menu-open",->
              a class:"pure-menu-heading", href:"/", "Komischfarben"
              @userManagementMixinMenu{ul:"pure-menu-list",li:"pure-menu-item",a:"pure-menu-link", active_li:"pure-menu-selected"}
              ul class:"pure-menu-list", ->
                li class:"pure-menu-item",->
                  a class:"pure-menu-link",href:"https://github.com/nonchip/komischfarben2",style:"color:#555",target:"_blank","Github"
                li class:"pure-menu-item",->
                  a class:"pure-menu-link",href:"https://github.com/nonchip/komischfarben2/issues",style:"color:#555",target:"_blank","Report issues"
          div id:"main",->
            if true or @has_content_for "header" then
              div class:"header",->
                tt "BETA"
                @content_for "header"
            div class:"content",->
              @content_for "inner"
        script src:"/static/js/main_layout.js"


youtube: new module. thanks to @einarjh for idea

Former-commit-id: 00ca019fc57fef283e211478960ce2f7f54b34d4 [formerly 540ff91c6646d283681cc44a2ffddf8d732bb64e]
Former-commit-id: bd2a416e81635f47b25efc97567c1fa881f01baf


weatherunderground: new module

weatherunderground: new module

Former-commit-id: c4c26b36b9f35dbce1b713ad5c7fdf124098aa4a [formerly 2e5e157d61c0dc472dd42b5b9c2d6c1f3a9ffd14]
Former-commit-id: 60dd01c3b30cb24d6bb79d6b59cce65ee43980f3

-- Weather Underground API module, requires a key set in configuration
-- WeatherUndergroundKey

util = require'util'
os = require'os'

key = ivar2.config.WeatherUndergroundKey
unless key
  return {}

geoApiBase = 'http://api.geonames.org/searchJSON?name=%s&featureClass=P&username=haste'
wugeolookupurl = 'http://api.wunderground.com/api/%s/geolookup/q/59.788,5.7218.json'
language = 'NO'

tempColor = (v) ->
  v = tonumber(v)
  if v > 0
    color = util.red
    return color(v)
  else
    color = util.lightblue
    return color(v)

windSpeedToName = (ws) ->
  windtext = ''
  unless ws
    return windtext
  if ws > 32
    windtext = 'orkan!'
  elseif ws > 28.5
    windtext = 'sterk storm'
  elseif ws > 24.5
    windtext = 'full storm'
  elseif ws > 20.8
    windtext = 'liten storm'
  elseif ws > 17.2
    windtext = 'sterk kuling'
  elseif ws > 13.9
    windtext = 'stiv kuling'
  elseif ws > 10.8
    windtext = 'liten kuling'
  elseif ws > 8
    windtext = 'frisk bris'
  elseif ws > 5.5
    windtext = 'laber bris'
  elseif ws > 3.4
    windtext = 'lett bris'
  elseif ws > 1.5
    windtext = 'svak vind'
  elseif ws > 0.3
    windtext = 'flau vind'
  else
    windtext = 'vindstille'
  return windtext

findLocation = (source, destination, name, cb) =>
  gurl = geoApiBase\format(util.urlEncode(name))
  util.simplehttp gurl, (data) ->
    json = util.json.decode(data)
    if json.totalResultsCount == 0
      return @Msg('privmsg', destination, source, "Does that place even exist?")
    city = json.geonames[1]
    lat = city.lat
    lon = city.lng
    cb(lon, lat)

lookupConditions = (source, destination, input, pws) =>
  findLocation @, source, destination, input, (lon, lat) ->
    pws = pws or false
    if pws
      pws = 'pws:1/'
    else
      pws = 'pws:0/'
    url = "http://api.wunderground.com/api/#{key}/lang:#{language}/#{pws}conditions/q/#{lat},#{lon}.json"
    @Log 'debug', "Fetching WU URL: %s", url
    util.simplehttp url, (data) ->
      json = util.json.decode(data)
      if json.error ~= nil and json.error.description ~=nil
        return say json.error.description
      current = json.current_observation

      location = current.observation_location
      city = location.city

      weather = current.weather
      temp = current.temp_c
      feelsLike = current.feelslike_c

      windSpeedname = windSpeedToName(current.wind_kph/3.6)

      windDirection = current.wind_dir

      rain = current.precip_today_metric

      time = os.date('%H:%M', tonumber(current.observation_epoch))

      @Msg 'privmsg', destination, source, '%s, %s °C (følt %s °C), %s %s, %s mm, (%s, %s)', weather, tempColor(temp), tempColor(feelsLike), windDirection, windSpeedname, rain, city, time

lookupConditionsPWS = (source, destination, input) =>
  lookupConditions(@, source, destination, input, true)

PRIVMSG:
  '^%pwu (.+)$': lookupConditions
  '^%ppws (.+)$': lookupConditionsPWS

error: pathspec 'modules/weatherunderground.moon' did not match any file(s) known to git


list: new module, list commands and patterns in commands

Former-commit-id: 052bfa88b9a843a9690bdc2952d1c82771b12bfc [formerly 5daa69fb418388f0911927e58832fc2dcb0c43d5]
Former-commit-id: 5a613df21bb786961bb8a6a7c8e9ce0451cc9466

listModules = (source, destination, arg) =>
  out = {}
  for moduleName, moduleTable in next, @Events!['PRIVMSG']
    unless @IsModuleDisabled moduleName, destination
      table.insert out, moduleName
  if #out > 0
    @Msg 'privmsg', destination, source, "Modules: %s", table.concat(out, ', ')

listPatterns = (source, destination, moduleName) =>
  moduleTable = @Events!['PRIVMSG'][moduleName]
  unless moduleTable
    return

  if @IsModuleDisabled moduleName, destination
    return

  out = {}
  for pattern, callback in next, moduleTable
    table.insert out, pattern

  if #out > 0
    @Msg 'privmsg', destination, source, "%s patterns: %s", moduleName, table.concat(out, ', ')

PRIVMSG:
  '%plist$': listModules
  '%plist (%w+)$': listPatterns

error: pathspec 'modules/list.moon' did not match any file(s) known to git


emoji: new module to print emojis

emoji: new module to print emojis

Former-commit-id: 4f0be689fa52c2c6fdac52481009bdec5e607814 [formerly cd5f651d7e19fe6988ead497a7ef2de22dbc96f7]
Former-commit-id: 9567b09860815aac22bd591e5af62ac5a17c6255

emojis =
  emoticons:'😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏
    😐😑😒😓😔😕😖😗😘😙😚😛😜😝😞😟
    😠😡😢😣😤😥😦😧😨😩😪😫😬😭😮😯
    😰😱😲😳😴😵😶😷😸😹😺😻😼😽😾😿
    🙀🙅🙆🙇🙈🙉🙊🙋🙌🙍🙎🙏',
  transport:'🚀🚁🚂🚃🚄🚅🚆🚇🚈🚉🚊🚋🚌🚍🚎🚏
    🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🚝🚞🚟
    🚠🚡🚢🚣🚤🚥🚦🚧🚨🚩🚪🚫🚬🚭🚮🚯
    🚰🚱🚲🚳🚴🚵🚶🚷🚸🚹🚺🚻🚼🚽🚾🚿
    🛀🛁🛂🛃🛄🛅',
  symbol:'☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏
    ☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟
    ☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯
    ☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿
    ♀♁♂♃♄♅♆♇♈♉♊♋♌♍♎♏
    ♐♑♒♓♔♕♖♗♘♙♚♛♜♝♞♟
    ♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯
    ♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿
    ⚀⚁⚂⚃⚄⚅⚆⚇⚈⚉⚊⚋⚌⚍⚎⚏
    ⚐⚑⚒⚓⚔⚕⚖⚗⚘⚙⚚⚛⚜⚝⚞⚟
    ⚠⚡⚢⚣⚤⚥⚦⚧⚨⚩⚪⚫⚬⚭⚮⚯
    ⚰⚱⚲⚳⚴⚵⚶⚷⚸⚹⚺⚻⚼⚽⚾⚿
    ⛀⛁⛂⛃⛄⛅⛆⛇⛈⛉⛊⛋⛌⛍⛎⛏
    ⛐⛑⛒⛓⛔⛕⛖⛗⛘⛙⛚⛛⛜⛝⛞⛟
    ⛠⛡⛢⛣⛤⛥⛦⛧⛨⛩⛪⛫⛬⛭⛮⛯
    ⛰⛱⛲⛳⛴⛵⛶⛷⛸⛹⛺⛻⛼⛽⛾⛿',
  misc:'🌀🌁🌂🌃🌄🌅🌆🌇🌈🌉🌊🌋🌌🌍🌎🌏
    🌐🌑🌒🌓🌔🌕🌖🌗🌘🌙🌚🌛🌜🌝🌞🌟
    🌠
    🌰🌱🌲🌳🌴🌵🌷🌸🌹🌺🌻🌼🌽🌾🌿
    🍀🍁🍂🍃🍄🍅🍆🍇🍈🍉🍊🍋🍌🍍🍎🍏
    🍐🍑🍒🍓🍔🍕🍖🍗🍘🍙🍚🍛🍜🍝🍞🍟
    🍠🍡🍢🍣🍤🍥🍦🍧🍨🍩🍪🍫🍬🍭🍮🍯
    🍰🍱🍲🍳🍴🍵🍶🍷🍸🍹🍺🍻🍼
    🎀🎁🎂🎃🎄🎅🎆🎇🎈🎉🎊🎋🎌🎍🎎🎏
    🎐🎑🎒🎓
    🎠🎡🎢🎣🎤🎥🎦🎧🎨🎩🎪🎫🎬🎭🎮🎯
    🎰🎱🎲🎳🎴🎵🎶🎷🎸🎹🎺🎻🎼🎽🎾🎿
    🏀🏁🏂🏃🏄🏆🏇🏈🏉🏊

    🏠🏡🏢🏣🏤🏥🏦🏧🏨🏩🏪🏫🏬🏭🏮🏯
    🏰
    🐀🐁🐂🐃🐄🐅🐆🐇🐈🐉🐊🐋🐌🐍🐎🐏
    🐐🐑🐒🐓🐔🐕🐖🐗🐘🐙🐚🐛🐜🐝🐞🐟
    🐠🐡🐢🐣🐤🐥🐦🐧🐨🐩🐪🐫🐬🐭🐮🐯
    🐰🐱🐲🐳🐴🐵🐶🐷🐸🐹🐺🐻🐼🐽🐾
    👀👂👃👄👅👆👇👈👉👊👋👌👍👎👏
    👐👑👒👓👔👕👖👗👘👙👚👛👜👝👞👟
    👠👡👢👣👤👥👦👧👨👩👪👫👬👭👮👯
    👰👱👲👳👴👵👶👷👸👹👺👻👼👽👾👿
    💀💁💂💃💄💅💆💇💈💉💊💋💌💍💎💏
    💐💑💒💓💔💕💖💗💘💙💚💛💜💝💞💟
    💠💡💢💣💤💥💦💧💨💩💪💫💬💭💮💯
    💰💱💲💳💴💵💶💷💸💹💺💻💼💽💾💿
    📀📁📂📃📄📅📆📇📈📉📊📋📌📍📎📏
    📐📑📒📓📔📕📖📗📘📙📚📛📜📝📞📟
    📠📡📢📣📤📥📦📧📨📩📪📫📬📭📮📯
    📰📱📲📳📴📵📶📷📹📺📻📼
    🔀🔁🔂🔃🔄🔅🔆🔇🔈🔉🔊🔋🔌🔍🔎🔏
    🔐🔑🔒🔓🔔🔕🔖🔗🔘🔙🔚🔛🔜🔝🔞🔟
    🔠🔡🔢🔣🔤🔥🔦🔧🔨🔩🔪🔫🔬🔭🔮🔯
    🔰🔱🔲🔳🔴🔵🔶🔷🔸🔹🔺🔻🔼🔽
    🕀🕁🕂🕃
    🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛🕜🕝🕞🕟
    🕠🕡🕢🕣🕤🕥🕦🕧
    🗻🗼🗽🗾🗿'

for cat, str in pairs emojis
  str, n = str\gsub('%s', '')
  emojis[cat] = str

PRIVMSG:
  '^%pemoji all$': (source, dest, arg) =>
    out = {}
    for cat, str in pairs emojis
      table.insert out, str
    @Msg 'privmsg', dest, source, table.concat out
  '^%pemoji$': (source, dest, arg) =>
    out = {}
    for cat, str in pairs emojis
      table.insert out, cat
    @Msg 'privmsg', dest, source, table.concat(out, ', ')
  '^%pemoji (.+)$': (source, dest, cat) =>
    @Msg 'privmsg', dest, source, emojis[cat]

error: pathspec 'modules/emoji.moon' did not match any file(s) known to git


Added more tests for `random` function

Added more tests for `random` function

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/SuperPaintman/umka.git/'


logger: log incoming messages to postgresql

Former-commit-id: 9e0b7a1e0957c9e2affc2c93af753229688a646a [formerly b114afd7472464074c2fac7c5bb8c5ea2bd4203a]
Former-commit-id: 2f690d629e9680fd6e175fa89899aff37a48e485

dbi = require 'DBI'
require'logging.console'
log = logging.console()

conn = false

schemas = {[[
  CREATE TABLE IF NOT EXISTS log (
    time timestamp DEFAULT now(),
    nick text,
    channel text,
    message text,
    type text
  );]],
  [[CREATE INDEX idx_time ON log(time);]],
  [[CREATE INDEX idx_channel ON log(channel);]]
}

connect = ->
  conn, err = DBI.Connect('PostgreSQL', ivar2.config.dbname, ivar2.config.dbuser, ivar2.config.dbpass, ivar2.config.dbhost, ivar2.config.dbport)
  unless conn
    log\error "Unable to connect to DB: #{err}"
    return

  conn\autocommit(true)

  --for s in *schemas
  --  a,b = DBI.Do(conn, s)

dbh = ->
  connect! unless conn

  -- Check if connection is alive
  alive = conn\ping!
  connect! unless alive

  success, err = DBI.Do(conn, 'SELECT NOW()')
  unless success
    log\error "SQL Connection :#{err}"
    connect!

  return conn

dblog = (type, source, destination, arg) =>
  nick = source.nick

  if type == 'PRIVMSG'
    -- action
    if arg\sub(1,1) == '\001' and arg\sub(-1) == '\001'
      arg = arg\sub(9, -2)
    type = 'ACTION'

  unless arg
    arg = ''

  insert = dbh!\prepare('INSERT INTO log(nick,channel,message,type) values(?,?,?,?)')
  stmt, err = insert\execute(nick, destination, arg, type)
  unless stmt
    log\error err

return {
  PRIVMSG: {
    (s, d, a) =>
      dblog @, 'PRIVMSG', s, d, a
  }
  NOTICE: {
    (s, d, a) =>
      dblog @, 'NOTICE', s, d, a
  }
  JOIN: {
    (s, d, a) =>
      dblog @, 'JOIN', s, d, a
  }
  PART: {
    (s, d, a) =>
      dblog @, 'PART', s, d, a
  }
  KICK: {
    (s, d, a) =>
      dblog @, 'KICK', s, d, a
  }
  MODE: {
    (s, d, a) =>
      dblog @, 'MODE', s, d, a
  }
  TOPIC: {
    (s, d, a) =>
      dblog @, 'TOPIC', s, d, a
  }
  NICK: {
    (s, d, a) =>
      dblog @, 'NICK', s, d, a
  }
  QUIT: {
    (s, d, a) =>
      dblog @, 'QUIT', s, d, a
  }
}

error: pathspec 'modules/logger.moon' did not match any file(s) known to git


autoop: new module, autoop owners and chan owners

Former-commit-id: f0aede26ccc3e08df91af76ad08bf97fd601b51d [formerly 6fb23ebd2b6bd08b5c599ab2c162f32746b27110]
Former-commit-id: 9f70636f04b2f37bf9785464209d8504cbef832c


plugins/sasl: Initial Commit

plugins/sasl: Initial Commit

import to_base64 from require 'basexx'
import IRCClient from require 'irc'

handlers =
	PLAIN: =>
		c = coroutine.create (auth)=>
			prefix, command, args, trail = coroutine.yield "AUTHENTICATE PLAIN"
			assert args[#args] == "+", "Unable to continue: #{table.concat(args)}"
			payload = ""
			payload ..= "#{auth.identity or auth.username}\000"
			payload ..= "#{auth.username}\000#{auth.password}"
			payload = "AUTHENTICATE #{to_base64 payload}"
			prefix, command, args, trail = coroutine.yield payload
			print command
			assert command == "903" or command == "900", trail

			-- destroy self on finish
			@fire_hook 'CAP_ACK' -- finished SASL, release lock on CAP END
			@sasl_handler = nil

		@send_raw select(2, assert(coroutine.resume c, self, @config.auth))
		return c

IRCClient\add_hook 'CAP_ACK.sasl', (values = 'PLAIN')=>
	return unless @config.auth
	methods = [w for w in values\gmatch "[^,]+"]
	for method in *methods
		if handlers[method] and @config.auth.mechanism == method
			-- found usable method
			@set_caps += 1
			@sasl_handler = handlers[method] self, @config.auth

for command in *{"AUTHENTICATE", "900", "901", "903", "904", "905", "906"}
	IRCClient\add_handler command, (prefix, args, trail)=>
		return if not @sasl_handler
		ok, result = coroutine.resume @sasl_handler, prefix, command, args, trail
		if not ok
			@sasl_handler = nil
			@fire_hook 'CAP_ACK' -- failed SASL, release lock on CAP END
			error "Error in SASL mechanism: #{result}"
		else
			@send_raw result

error: pathspec 'plugins/sasl.moon' did not match any file(s) known to git


trigger: first version

trigger: first version

Former-commit-id: 60a3eb9e39dbde2bd28dc103f12ea702d55f7761 [formerly 599e093e56fde346341b6676a1c6c4104e12c65a]
Former-commit-id: f1a09870622de487e212493e36d5413a6776a3b3

sql = require'lsqlite3'

openDb = ->
  db = sql.open 'cache/trigger.sql'

  db\exec [[
    CREATE TABLE IF NOT EXISTS trigger (
      pattern UNIQUE ON CONFLICT REPLACE,
      funcstr
    );
    ]]
  return db

triggerHelp  = (source, destination, argument) =>
   @Msg 'privmsg', source, destination, 'Usage: !trigger add <lua pattern>|<lua code>. Ex: !trigger add !help (%w+)|say "%s, you need help'

-- Construct a safe environ for trigger with two functions; say and reply
--- TODO: actually make it safe :-)
--- Preferrably you should be able to cross-call modules here to make aliasing possible
sandbox = (func) ->
  (source, destination, ...) =>
    -- Store args so we can use them in env
    arg = {...}
    env =
      say: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      reply: (str) ->
        @Msg 'privmsg', destination, source, source.nick..': '..str, unpack(arg),
      print: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      simplehttp:require'simplehttp'
      json:require'json'
      :string
      :type
      :tostring
      :tonumber
      :ipars
      :pairs
      :table
      :next

    --setfenv func, setmetatable(env, {__index: _G })
    setfenv func, env
    success, err = pcall func, arg
    if err
      @Msg 'privmsg', destination, source, err
    else
      success

-- Construct a safe function to run and return a handler
triggerHandler = (source, destination, funcstr) =>
  func, err = loadstring funcstr
  unless func
    @Msg 'privmsg', destination, source, 'Trigger error: %s', err
  else
    sandbox(func)

-- Stub to create a trigger name in event table
handlerName = (pattern) ->
  "trigger#{pattern}"

-- Register the command
regCommand = (source, destination, pattern, funcstr) =>
  db = openDb!
  ins = db\prepare "INSERT INTO trigger (pattern, funcstr) VALUES(?, ?)"
  code = ins\bind_values pattern, funcstr
  code = ins\step!
  code = ins\finalize!
  db\close!

  @RegisterCommand handlerName(pattern), pattern, triggerHandler(@, source, destination, funcstr)

delCommand = (source, destination, pattern) =>
  db = openDb!
  ins = db\prepare "DELETE FROM trigger WHERE pattern = ?"
  code = ins\bind_values pattern
  code = ins\step!
  code = ins\finalize!
  db\close!
  @UnregisterCommand handlerName(pattern)

-- Register commands on startup
db = openDb!
for row in db\nrows 'SELECT pattern, funcstr FROM trigger'
  print ivar2
  ivar2\RegisterCommand handlerName(row.pattern), row.pattern, triggerHandler(ivar2, nil, nil, row.funcstr)

PRIVMSG:
  '%ptrigger$': triggerHelp
  '%ptrigger add (.+)|(.+)$': regCommand
  '%ptrigger del (.+)$': delCommand

error: pathspec 'modules/trigger.moon' did not match any file(s) known to git


charset: new module to help people detect charset

iconv = require'iconv'

iso2utf = iconv.new('UTF-8'.."//TRANSLIT", 'ISO-8859-1')
utf2iso = iconv.new('ISO-8859-1'.."//TRANSLIT", 'UTF-8')

toUtf = (s) ->
  iso2utf\iconv(s)

toIso = (s) ->
  utf2iso\iconv(s)

latinpattern = "^#{toIso'æøå'}%??"

PRIVMSG:
  '^æøå%??$': =>
    reply "UTF-8. Relax, you're doing fine."
  latinpattern: =>
    reply 'ISO-8559-1. Pls stahp.'
  '^Ã¦Ã¸Ã¥%??$': =>
    reply 'WTF8. Is very nice.'
  '^fxe%??$': =>
    reply 'fxe is Latin1 with 8th bit stripped'
  '^{%|}%??$': =>
    reply 'du sender NS-4551-1 (aka. ISO-646-NO).. og det er jo litt ut.'
  '^��?%??$': =>
    reply 'du sender replacement chars.'
  '^���?%??$': =>
    reply 'du sender replacement chars.'
  '^+AOYA+ADl?%??$': =>
    reply 'du sender UTF-7'
  '^%piconv (.+) (.+) (.+)$': (source, dest, frm, to, arg) =>
    convert = iconv.new("#{to\upper!}//TRANSLIT", frm\upper!)
    unless convert
      return say 'Invalid encoding given'
    nstr, err = convert\iconv(arg)
    if err
      return say err

    say nstr

error: pathspec 'modules/charset.moon' did not match any file(s) known to git


illness: new module

illness: new module

Former-commit-id: a9afdc38d22534f39ed54d5d39abbc042250b9df [formerly 2f11e2493272528f0c2810da9fab8d7f7e25e142]
Former-commit-id: a9a5707709aa6aa5858cd1bbe39daf4f4813a191

-- thanks to byte[] for sicknesses :-)

illnesses = {
  [[nany vgpuvat]],
  [[nany cnva]],
  [[nfgvtzngvfz]],
  [[onq oerngu]],
  [[oryyl ohggba qvfpunetr]],
  [[oyheerq ivfvba pnhfrf]],
  [[oyheel ivfvba]],
  [[oyheel ivfvba]],
  [[objry vapbagvarapr]],
  [[pbyq fberf]],
  [[pbafgvcngvba]],
  [[peno yvpr]],
  [[qnaqehss]],
  [[qvfrnfrf bs gur rlr]],
  [[rwnphyngvba]],
  [[rerpgvba ceboyrzf]],
  [[rlr pbaqvgvbaf]],
  [[rlr qvfrnfrf]],
  [[rlr rkrepvfrf]],
  [[rlr synfurf]],
  [[rlr sybngref]],
  [[rlr urnygu]],
  [[rlr erqarff]],
  [[rlr fgenva flzcgbzf]],
  [[rlr ivfvba]],
  [[rlr ivgnzvaf]],
  [[srpny vapbagvarapr]],
  [[svffher]],
  [[synxl fxva]],
  [[sybngref va gur rlr]],
  [[travgny jnegf]],
  [[tynhpbzn flzcgbzf]],
  [[unve ybff]],
  [[urnq yvpr]],
  [[urzzbeubvqf]],
  [[uvpphcf]],
  [[ubj gb vzcebir rlrfvtug]],
  [[vagenbphyne cerffher]],
  [[veevgnoyr objry flaqebzr]],
  [[vgpuvat]],
  [[wbpx vgpu]],
  [[ynfre rlr fhetrel]],
  [[ynfre fhetrel sbe rlrf]],
  [[ynfvx]],
  [[ynfvx rlr fhetrel]],
  [[ynfvx fhetrel pbfg]],
  [[zna obbof]],
  [[zrzbel ceboyrzf]],
  [[zrabcnhfr]],
  [[zbhgu hypref]],
  [[anvy ovgvat]],
  [[beny frk]],
  [[birejrvtug]],
  [[cvyrf]],
  [[cbylcunfvp fyrrc]],
  [[cbea nqqvpgvba]],
  [[cerzrafgehny flaqebzr]],
  [[erq snpr]],
  [[erfgyrff yrtf]],
  [[ergvany qrgnpuzrag]],
  [[fpnyl fxva]],
  [[fpnef]],
  [[funxl unaqf]],
  [[fzryyl srrg]],
  [[fabevat]],
  [[fgnzzrevat]],
  [[fgergpu znexf]],
  [[fjrngvat]],
  [[gbranvy vasrpgvba]],
  [[gbathr ceboyrzf]],
  [[hevanel vapbagvarapr]],
  [[inevpbfr irvaf]],
  [[ireehpnf]],
  [[ivfvba gurencl]],
  [[jvaq]],
}

illness = ->
  ivar2.util.rot13(illnesses[math.random(1, #illnesses)])

PRIVMSG:
  '^%pillness (.+)$': (s, d, nick) =>
    say "#{nick}, afflicted by #{illness!}"
  '^%pillness$': (s, d) =>
    say illness!

error: pathspec 'modules/illness.moon' did not match any file(s) known to git


aunesand: new module

aunesand: new module

html2unicode = require'html'

hex_to_char = (x) ->
  string.char(tonumber(x, 16))

unescape = (url) ->
  url\gsub("%%(%x%x)", hex_to_char)

PRIVMSG:
  '^%paunesand': (source, destination) =>
    rnd = math.random(1,3000)
    ivar2.util.simplehttp 'http://p3.no/uv/aune-sands-poesigenerator/?cachebuster='..rnd, (html) ->
      match = html\match[[<a title="Del diktet på Facebook" href="(.*)"]]
      if match
        poem = match\match[[facebookdescription=(.-)-]]
        poem = poem\gsub '+', ' '
        poem = unescape(poem)
        if poem
          say html2unicode(poem)

error: pathspec 'modules/aunesand.moon' did not match any file(s) known to git


navnedag: module for norwegian name days

os = require'os'
util = require'util'
data =
  "01.01":"Jesus"
  "02.01":"Dagfinn Dagfrid"
  "03.01":"Alfred Alf"
  "04.01":"Roar Roger"
  "05.01":"Hanna Hanne"
  "06.01":"Aslaug Åslaug"
  "07.01":"Eldbjørg Knut"
  "08.01":"Torfinn Turid"
  "09.01":"Gunnar Gunn"
  "10.01":"Sigmund Sigrun"
  "11.01":"Børge Børre"
  "12.01":"Reinhard Reinert"
  "13.01":"Gisle Gislaug"
  "14.01":"Herbjørn Herbjørg"
  "15.01":"Laurits Laura"
  "16.01":"Hjalmar Hilmar"
  "17.01":"Anton Tønnes Tony"
  "18.01":"Hildur Hild"
  "19.01":"Marius Margunn"
  "20.01":"Fabian Sebastian Bastian"
  "21.01":"Agnes Agnete"
  "22.01":"Ivan Vanja"
  "23.01":"Emil Emilie Emma"
  "24.01":"Joar Jarle Jarl"
  "25.01":"Paul Pål"
  "26.01":"Øystein Esten"
  "27.01":"Gaute Gurly Gry"
  "28.01":"Karl Karoline"
  "29.01":"Herdis Hermod Hermann"
  "30.01":"Gunnhild Gunda"
  "31.01":"Idun Ivar"
  "01.02":"Birte Bjarte"
  "02.02":"Jomar Jostein"
  "03.02":"Ansgar Asgeir"
  "04.02":"Veronika Vera"
  "05.02":"Agate Ågot"
  "06.02":"Dortea Dorte"
  "07.02":"Rikard Rigmor Riborg"
  "08.02":"Åshild Åsne"
  "09.02":"Lone Leikny"
  "10.02":"Ingfrid Ingrid"
  "11.02":"Ingve Yngve"
  "12.02":"Randulf Randi Ronja"
  "13.02":"Svanhild Svanlaug"
  "14.02":"Hjørdis Jardar"
  "15.02":"Sigfred Sigbjørn"
  "16.02":"Julian Juliane Jill"
  "17.02":"Aleksandra Sandra Sondre"
  "18.02":"Frøydis Frode"
  "19.02":"Ella Elna"
  "20.02":"Halldis Halldor"
  "21.02":"Samuel Selma Celine"
  "22.02":"Tina Tine Tim"
  "23.02":"Torstein Torunn"
  "24.02":"Mattias Mattis Mats"
  "25.02":"Viktor Viktoria"
  "26.02":"Inger Ingjerd"
  "27.02":"Laila Lill"
  "28.02":"Marina Maren"
  "01.03":"Audny Audun"
  "02.03":"Erna Ernst"
  "03.03":"Gunnbjørg Gunnveig"
  "04.03":"Ada Adrian"
  "05.03":"Patrick Patricia"
  "06.03":"Annfrid Andor"
  "07.03":"Arild Are"
  "08.03":"Beate Betty Bettina"
  "09.03":"Sverre Sindre"
  "10.03":"Edel Edle"
  "11.03":"Edvin Tale"
  "12.03":"Gregor Gro"
  "13.03":"Greta Grete"
  "14.03":"Mathilde Mette"
  "15.03":"Christel Christer Chris"
  "16.03":"Gudmund Gudny"
  "17.03":"Gjertrud Trude"
  "18.03":"Aleksander Sander Edvard"
  "19.03":"Josef Josefine"
  "20.03":"Joakim Kim"
  "21.03":"Bendik Bengt Bent"
  "22.03":"Paula Pauline"
  "23.03":"Gerda Gerd"
  "24.03":"Ulrikke Rikke"
  "25.03":"Maria Marie Mari"
  "26.03":"Gabriel Glenn"
  "27.03":"Rudolf Rudi"
  "28.03":"Åsta Åste"
  "29.03":"Jonas Jonatan"
  "30.03":"Holger Olga"
  "31.03":"Vebjørn Vegard"
  "01.04":"Aron Arve Arvid"
  "02.04":"Sigvard Sivert"
  "03.04":"Gunnvald Gunnvor"
  "04.04":"Nanna Nancy Nina"
  "05.04":"Irene Eirin Eril"
  "06.04":"Åsmund Asmund"
  "07.04":"Oddveig Oddvin"
  "08.04":"Asle Atle"
  "09.04":"Rannveig Rønnaug"
  "10.04":"Ingvald Ingveig"
  "11.04":"Ylva Ulf"
  "12.04":"Julius Julie"
  "13.04":"Asta Astrid"
  "14.04":"Ellinor Nora"
  "15.04":"Oda Odin Odd"
  "16.04":"Magnus Mons"
  "17.04":"Elise Else Elsa"
  "18.04":"Eilen Eilert"
  "19.04":"Arnfinn Arnstein"
  "20.04":"Kjellaug Kjellrun"
  "21.04":"Jeanette Jannike"
  "22.04":"Oddgeir Oddny"
  "23.04":"Georg Jørgen Jørg"
  "24.04":"Albert Olaug"
  "25.04":"Markus Mark"
  "26.04":"Terese Tea"
  "27.04":"Charles Charlotte Lotte"
  "28.04":"Vivi Vivian"
  "29.04":"Toralf Torolf"
  "30.04":"Gina Gitte"
  "01.05":"Filip Valborg"
  "02.05":"Åsa Åse"
  "03.05":"Gjermund Gøril"
  "04.05":"Monika Mona"
  "05.05":"Gudbrand Gullborg"
  "06.05":"Guri Gyri"
  "07.05":"Maia Mai Maiken"
  "08.05":"Åge Åke"
  "09.05":"Kasper Jesper"
  "10.05":"Asbjørg Asbjørn Espen"
  "11.05":"Magda Malvin"
  "12.05":"Normann Norvald"
  "13.05":"Linda Line Linn"
  "14.05":"Kristian Kristen Karsten"
  "15.05":"Hallvard Hallvor"
  "16.05":"Sara Siren"
  "17.05":"Harald Ragnhild"
  "18.05":"Eirik Erik Erika"
  "19.05":"Torjus Torje Truls"
  "20.05":"Bjørnar Bjørnhild"
  "21.05":"Helene Ellen Eli"
  "22.05":"Henning Henny"
  "23.05":"Oddleif Oddlaug"
  "24.05":"Ester Iris"
  "25.05":"Ragna Ragnar"
  "26.05":"Annbjørg Annlaug"
  "27.05":"Katinka Cato"
  "28.05":"Vilhelm William Willy"
  "29.05":"Magnar Magnhild"
  "30.05":"Gard Geir"
  "31.05":"Pernille Preben"
  "01.06":"June Juni"
  "02.06":"Runa Runar Rune"
  "03.06":"Rasmus Rakel"
  "04.06":"Heidi Heid"
  "05.06":"Torbjørg Torbjørn Torben"
  "06.06":"Gustav Gyda"
  "07.06":"Robert Robin"
  "08.06":"Renate René"
  "09.06":"Kolbein Kolbjørn"
  "10.06":"Ingolf Ingunn"
  "11.06":"Borgar Bjørge Bjørg"
  "12.06":"Sigfrid Sigrid Siri"
  "13.06":"Tone Tonje Tanja"
  "14.06":"Erlend Erland"
  "15.06":"Vigdis Viggo"
  "16.06":"Torhild Toril Tiril"
  "17.06":"Botolf Bodil"
  "18.06":"Bjarne Bjørn"
  "19.06":"Erling Elling"
  "20.06":"Salve Sølve Sølvi"
  "21.06":"Agnar Annar"
  "22.06":"Håkon Maud"
  "23.06":"Elfrid Eldrid"
  "24.06":"Johannes Jon Hans"
  "25.06":"Jørund Jorunn"
  "26.06":"Jenny Jonny"
  "27.06":"Aina Ina Ine"
  "28.06":"Lea Leo Leon"
  "29.06":"Peter Petter Per"
  "30.06":"Solbjørg Solgunn"
  "01.07":"Ask Embla"
  "02.07":"Kjartan Kjellfrid"
  "03.07":"Andrea Andrine André"
  "04.07":"Ulrik Ulla"
  "05.07":"Mirjam Mina"
  "06.07":"Torgrim Torgunn"
  "07.07":"Håvard Hulda"
  "08.07":"Sunniva Synnøve Synne"
  "09.07":"Gøran Jøran Ørjan"
  "10.07":"Anita Anja"
  "11.07":"Kjetil Kjell"
  "12.07":"Elias Eldar"
  "13.07":"Mildrid Melissa Mia"
  "14.07":"Solfrid Solrun"
  "15.07":"Oddmund Oddrun"
  "16.07":"Susanne Sanna"
  "17.07":"Guttorm Gorm"
  "18.07":"Arnulf Ørnulf"
  "19.07":"Gerhard Gjert"
  "20.07":"Margareta Margit Marit"
  "21.07":"Johanne Janne Jane"
  "22.07":"Malene Malin Mali"
  "23.07":"Brita Brit Britt"
  "24.07":"Kristine Kristin Kristi"
  "25.07":"Jakob Jack Jim"
  "26.07":"Anna Anne Ane"
  "27.07":"Marita Rita"
  "28.07":"Reidar Reidun"
  "29.07":"Olav Ola Ole"
  "30.07":"Aurora Audhild Aud"
  "31.07":"Elin Eline"
  "01.08":"Peder Petra"
  "02.08":"Karen Karin"
  "03.08":"Oline Oliver Olve"
  "04.08":"Arnhild Arna Arnt"
  "05.08":"Osvald Oskar"
  "06.08":"Gunnlaug Gunnleiv"
  "07.08":"Didrik Doris"
  "08.08":"Evy Yvonne"
  "09.08":"Ronald Ronny"
  "10.08":"Lorents Lars Lasse"
  "11.08":"Torvald Tarald"
  "12.08":"Klara Camilla"
  "13.08":"Anny Anine Ann"
  "14.08":"Hallgeir Hallgjerd"
  "15.08":"Margot Mary Marielle"
  "16.08":"Brynjulf Brynhild"
  "17.08":"Verner Wenche"
  "18.08":"Tormod Torodd"
  "19.08":"Sigvald Sigve"
  "20.08":"Bernhard Bernt"
  "21.08":"Ragnvald Ragni"
  "22.08":"Harriet Harry"
  "23.08":"Signe Signy"
  "24.08":"Belinda Bertil"
  "25.08":"Ludvig Lovise Louise"
  "26.08":"Øyvind Eivind Even"
  "27.08":"Roald Rolf"
  "28.08":"Artur August"
  "29.08":"Johan Jone Jo"
  "30.08":"Benjamin Ben"
  "31.08":"Berta Berte"
  "01.09":"Solveig Solvår"
  "02.09":"Lisa Lise Liss"
  "03.09":"Alise Alvhild Vilde"
  "04.09":"Ida Idar"
  "05.09":"Brede Brian Njål"
  "06.09":"Sollaug Siril Siv"
  "07.09":"Regine Rose"
  "08.09":"Amalie Alma Allan"
  "09.09":"Trygve Tyra Trym"
  "10.09":"Tord Tor"
  "11.09":"Dagny Dag"
  "12.09":"Jofrid Jorid"
  "13.09":"Stian Stig"
  "14.09":"Ingebjørg Ingeborg"
  "15.09":"Aslak Eskil"
  "16.09":"Lillian Lilly"
  "17.09":"Hildebjørg Hildegunn"
  "18.09":"Henriette Henry"
  "19.09":"Konstanse Connie"
  "20.09":"Tobias Tage"
  "21.09":"Trine Trond"
  "22.09":"Kyrre Kåre"
  "23.09":"Snorre Snefrid"
  "24.09":"Jan Jens"
  "25.09":"Ingvar Yngvar"
  "26.09":"Einar Endre"
  "27.09":"Dagmar Dagrun"
  "28.09":"Lena Lene"
  "29.09":"Mikael Mikal Mikkel"
  "30.09":"Helga Helge Hege"
  "01.10":"Rebekka Remi"
  "02.10":"Live Liv"
  "03.10":"Evald Evelyn"
  "04.10":"Frans Frank"
  "05.10":"Brynjar Boye Bo"
  "06.10":"Målfrid Møyfrid"
  "07.10":"Birgitte Birgit Berit"
  "08.10":"Benedikte Bente"
  "09.10":"Leidulf Leif"
  "10.10":"Fridtjof Frida Frits"
  "11.10":"Kevin Kennet Kent"
  "12.10":"Valter Vibeke"
  "13.10":"Torgeir Terje Tarjei"
  "14.10":"Kaia Kai"
  "15.10":"Hedvig Hedda"
  "16.10":"Flemming Finn"
  "17.10":"Marta Marte"
  "18.10":"Kjersti Kjerstin"
  "19.10":"Tora Tore"
  "20.10":"Henrik Heine Henrikke"
  "21.10":"Bergljot Birger"
  "22.10":"Karianne Karine Kine"
  "23.10":"Severin Søren"
  "24.10":"Eilif Eivor"
  "25.10":"Margrete Merete Märtha"
  "26.10":"Amandus Amanda"
  "27.10":"Sturla Sture"
  "28.10":"Simon Simen"
  "29.10":"Noralf Norunn"
  "30.10":"Aksel Åmund Ove"
  "31.10":"Edit Edna"
  "01.11":"Veslemøy Vetle"
  "02.11":"Tove Tuva"
  "03.11":"Raymond Roy"
  "04.11":"Otto Ottar"
  "05.11":"Egil Egon"
  "06.11":"Leonard Lennart"
  "07.11":"Ingebrigt Ingelin"
  "08.11":"Ingvild Yngvild"
  "09.11":"Tordis Teodor"
  "10.11":"Gudbjørg Gudveig"
  "11.11":"Martin Morten Martine"
  "12.11":"Torkjell Torkil"
  "13.11":"Kirsten Kirsti"
  "14.11":"Fredrik Fred Freddy"
  "15.11":"Oddfrid Oddvar"
  "16.11":"Edmund Edgar"
  "17.11":"Hugo Hogne Hauk"
  "18.11":"Magne Magny"
  "19.11":"Elisabet Lisbet"
  "20.11":"Halvdan Helle"
  "21.11":"Mariann Marianne"
  "22.11":"Cecilie Silje Sissel"
  "23.11":"Klement Klaus"
  "24.11":"Gudrun Guro"
  "25.11":"Katarina Katrine Kari"
  "26.11":"Konrad Kurt"
  "27.11":"Torlaug Torleif"
  "28.11":"Ruben Rut"
  "29.11":"Sofie Sonja"
  "30.11":"Andreas Anders"
  "01.12":"Arnold Arnljot Arnt"
  "02.12":"Borghild Borgny Bård"
  "03.12":"Sveinung Svein"
  "04.12":"Barbara Barbro"
  "05.12":"Stine Ståle"
  "06.12":"Nils Nikolai"
  "07.12":"Hallfrid Hallstein"
  "08.12":"Marlene Marion Morgan"
  "09.12":"Anniken Annette"
  "10.12":"Judit og Jytte"
  "11.12":"Daniel Dan"
  "12.12":"Pia Peggy"
  "13.12":"Lucia Lydia"
  "14.12":"Steinar Stein"
  "15.12":"Hilda Hilde"
  "16.12":"Oddbjørg Oddbjørn"
  "17.12":"Inga Inge"
  "18.12":"Kristoffer Kate"
  "19.12":"Iselin Isak"
  "20.12":"Abraham Amund"
  "21.12":"Tomas Tom Tommy"
  "22.12":"Ingemar Ingar"
  "23.12":"Sigurd Sjur"
  "24.12":"Adam Eva"
  "25.12":"Ingen navnedag"
  "26.12":"Stefan Steffen"
  "27.12":"Narve Natalie"
  "28.12":"Unni Une Unn"
  "29.12":"Vidar Vemund"
  "30.12":"David Diana Dina"
  "31.12":"Sylfest Sylvia Sylvi"

PRIVMSG:
  "^%pnavnedag$": (source, destination) =>
    now = os.date'%d.%m'
    names = table.concat(util.split(data[now], ' '), ', ')
    say names
  "^%pnavnedag (%d+).(%d+)$": (source, destination, day, month) =>
    day = string.format('%02d', day)
    month = string.format('%02d', month)
    now = day .. '.' .. month
    names = table.concat(util.split(data[now], ' '), ', ')
    say names

error: pathspec 'modules/navnedag.moon' did not match any file(s) known to git


giphy: new module

giphy: new module

api_key = 'dc6zaTOxFJmzC'

PRIVMSG:
  '^%pgiphy (.+)': (source, destination, arg) =>
      ivar2.util.simplehttp "http://api.giphy.com/v1/gifs/search?q=#{ivar2.util.urlEncode arg}&api_key=#{api_key}", (json) ->
        data = ivar2.util.json.decode json
        if not data then return

        url = data.data[1].images.original.url
        say url

error: pathspec 'modules/giphy.moon' did not match any file(s) known to git


object#last: fix value order

object#last: fix value order

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/alexherbo2/funcmay.moon.git/'


imageupload: new module for easy media sharing to IRC

Former-commit-id: 38b1f451147525901b31c4b58315fee2d33b3f56 [formerly e985517d229eafdb2fbfdb971ee78abc6a20e8d3]
Former-commit-id: 0cae01495cd5385bcb2842b3c39c87af7a9618f5

error: pathspec 'modules/imageupload.moon' did not match any file(s) known to git


rss: simple module for subscribing to rss feeds

Former-commit-id: 49818d2089a12783f887d7faf3bf100d9dc0884f [formerly 74abac0c5291837e6d9b3cb43554f0bb70061890]
Former-commit-id: 1e919fd0cd445d257d03aa9e7c03d28c536452eb

-- Module for subscribing to RSS and announce to channels
feedparser = require 'feedparser' -- http://feedparser.luaforge.net/
{:simplehttp, :json, :urlEncode, :bold} = require'util'
html2unicode = require 'html'

moduleName = 'rss'
key = moduleName
store = ivar2.persist

-- Function that add functionality to extract specific information for specific sites
feedSpecific = (link, id, entry) ->
  if link == 'https://comics.io/my/'

    title = entry.summary\match '
(.-)
' or ''
    img, alt = entry.summary\match '
'
    return "#{html2unicode title} #{img} #{html2unicode alt}"

  return "#{entry.title} - #{entry.link}"

poll = ->
  for c,_ in pairs(ivar2.channels)
    channelKey = key..':'..c
    channels = store[channelKey] or {}
    -- TODO separate channel logic for polling logic for the cases where
    -- two channels subscribe to the same feed
    for name, channel in pairs(channels)
      lastKey = channelKey .. ':' .. name .. ':last'
      lastmKey = lastKey..'modified'
      last = store[lastKey] or ''
      lastlastModified = store[lastmKey]
      simplehttp {url:channel.url,headers:{'If-Modified-Since':lastlastModified}}, (data, url, response) ->
        lastModified = response.headers['Last-Modified']
        if not lastModified
          lastModified = response.headers['Date']

        store[lastmKey] = lastModified

        -- Unmodified content
        if response.status_code == 304
          -- return from the simplehttp callback
          return

        feed, err = feedparser.parse data
        if err
          ivar2\Log 'error', "#{moduleName}: Error during parsing: <#{err}> data for feed: <#{name}> with URL <#{channel.url}>"
        else
          out = {}
          for i, e in pairs(feed.entries)
            -- Attempt to get a unique entry ID
            guid = e.guid
            if not guid then guid = e.id
            if not guid then guid = e.link
            if not guid
              ivar2\Log 'error', "#{moduleName}: No GUID when parsing entry: <#{e}> data for feed: <#{name}> with URL <#{channel.url}>"

            if guid == last
              break

            table.insert out, "[#{bold name}]: #{feedSpecific(feed.feed.id, feed.feed.link, e)}"

            -- First entry is the newest entry, look for that one on the
            -- next iteration
            if i == 1
              store[lastKey] = guid

            -- First time
            if last == ''
              break

          if #out > 0
            say "RSS "..table.concat(out, ', ')

subscribe = (source, destination, name, url) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  channels[name] = {channel:destination, name:name, url:url}
  store[channelKey] = channels
  reply "Ok. Subscribed to #{bold name}"
  poll!

unsubscribe = (source, destination, name) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  unless channels[name]
    reply "Wasn't subscribed. But, sure."
  else
    channels[name] = nil
    store[channelKey] = channels
    lastKey = channelKey .. ':' .. name .. ':last'
    store[lastKey] = nil
    reply "Ok. Stopped caring about #{bold name}"

list = (source, destination) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  out = {}
  for name, game in pairs(channels)
      out[#out+1] = name
  say "Subscribed to: #{table.concat(out, ', ')}"

-- Start the subscribe poller
ivar2\Timer(moduleName, 300, 300, poll)

PRIVMSG:
  '^%prss latest (.*)': getLatest
  '^%prss subscribe (.-) (.*)': subscribe
  '^%prss unsubscribe (.*)': unsubscribe
  '^%prss list': list
  -- for debugging
  '^%prss poll': poll

error: pathspec 'modules/rss.moon' did not match any file(s) known to git


Add example Aegisub automation script.

Add example Aegisub automation script.

-- This example is unlicensed under CC0

export script_name        = "ASSInspector Example"
export script_description = "Calculates bounding rectangles on the start and end times of selected events."
export script_author      = "torque"
export script_version     = 0x000003

ffi = require( 'ffi' )
log = require( 'a-mo.log' )

ffi.cdef( [[
typedef struct {
	int32_t x, y;
	uint32_t w, h;
} ASSI_Rect;

uint32_t    assi_getVersion( void );
void*       assi_init( int, int, const char*, uint32_t );
const char* assi_getErrorString( void* );
int         assi_setScript( void*, const char*, uint32_t, const char *, const uint32_t );
int         assi_calculateBounds( void*, ASSI_Rect*, const int32_t*, const uint32_t );
void        assi_cleanup( void* );
]] )

-- Figure out a better way to load this?
extension = ('OSX' == ffi.os and '.dylib' or 'Windows' == ffi.os and '.dll' or '.so')
ASSInspector = ffi.load( aegisub.decode_path( "?user/automation/include/OptimASS/libASSInspector" .. extension ) )

logRect = ( rect ) ->
	log.dump( {
		x: rect.x
		y: rect.y
		w: rect.w
		h: rect.h
	} )

mainFunction = ( subtitle, selectedLines, activeLine ) ->
	if ASSInspector.assi_getVersion( ) < script_version
		log.windowError( "Installed ASSInspector library is outdated. Please update it." )
		return selectedLines
	elseif ASSInspector.assi_getVersion( ) > script_version
		log.windowError( "Installed OptimASS is outdated. Please update it." )
		return selectedLines

	scriptHeader = {
		"[Script Info]"
	}

	-- These are the only header fields that actually affect the way ASS
	-- is rendered. I don't actually know if ScriptType makes a matters.
	infoFields = {
		PlayResX:   true
		PlayResY:   true
		WrapStyle:  true
		ScriptType: true
		ScaledBorderAndShadow: true
	}

	styles = { }

	-- If there is no video (or, at minimum, timecodes) loaded, this will
	-- return nil.
	hasTimecodes = aegisub.frame_from_ms( 0 )

	-- Copy these functions into local variables to shorten them.
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	subtitleLen = #subtitle
	seenStyles = false
	resX = 640
	resY = 480

	-- This loop assumes the subtitle script is nicely organized in the
	-- order [Script Info] -> [V4+ Styles] -> [Events]. This isn't
	-- guaranteed for scripts floating around in the wild, but fortunately
	-- Aegisub 3.2 seems to do a very good job of ensuring this order when
	-- the script is loaded. In other words, this loop is probably only
	-- safe to do from within automation.
	for index = 1, subtitleLen
		with line = subtitle[index]
			if "info" == .class
				if infoFields[.key]
					table.insert( scriptHeader, .raw )

				if "PlayResX" == .key
					resX = tonumber( .value )
				elseif "PlayResY" == .key
					resY = tonumber( .value )

			elseif "style" == .class
				unless seenStyles
					table.insert( scriptHeader, "[V4+ Styles]\n" )
					seenStyles = true

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

	-- If a video is loaded, use its resolution instead of the script's
	-- resolution. Not convinced this is a great idea because people may
	-- use smaller workraws or something like that.
	vidResX, vidResY = aegisub.video_size( )
	resX = vidResX or resX
	resY = vidResY or resY

	minimalHeader = table.concat( scriptHeader, '\n' )
	inspector = ASSInspector.assi_init( resX, resY, minimalHeader, #minimalHeader )
	-- I honestly don't know if this comparison actually works.
	if nil == inspector
		log.windowError( "ASSInspector library init failed." )

	for eventIndex in *selectedLines
		with event = subtitle[eventIndex]
			-- This does not account for \r[name] being in the line.
			styles = (styles[.style] or "") .. "\n[Events]\n"
			events = .raw

			-- It occurs to me that perhaps having the user just pass in one
			-- string that contains both styles and events may be easier.
			if 0 < ASSInspector.assi_setScript( inspector, styles, #styles, events, #events )
				log.warn( assi_getErrorString( inspector ) )
				ASSInspector.assi_cleanup( inspector )
				aegisub.cancel( )

			-- We're going to render a line twice, once at its start and once
			-- at its end.
			renderCount = 2

			renderTimes = { }

			-- Cheat a bit when generating the times to render. This obviously
			-- will not scale with a renderCount > 2.
			if hasTimecodes
				-- Using the aegisub.frame_from_ms function pointers we copied
				-- earlier.
				startFrame = ffms( .start_time )
				endFrame   = ffms( .end_time ) - 1
				renderTimes[0] = math.floor( 0.5*( msff( startFrame ) + msff( startFrame + 1 ) ) )
				renderTimes[1] = math.floor( 0.5*( msff( endFrame ) + msff( endFrame + 1 ) ) )
			else
				renderTimes[0] = .start_time
				-- Trying to render exactly at end_time will result in a
				-- completely blank frame.
				renderTimes[1] = .end_time - 1

			-- Because we're rendering the line twice, we need two rects.
			rects = ffi.new( "ASSI_Rect[?]", renderCount )

			-- Allocate the array of times for rendering.
			times = ffi.new( "int32_t[?]", renderCount )

			-- Set up the render times. There's no real reason to do this in a
			-- loop if you know beforehand you're only going to render the
			-- line twice, but loops are more generic.
			for render = 0, renderCount - 1
				times[render] = renderTimes[render]

			-- Send the data in for rendering and bounds calculation.
			if 0 < ASSInspector.assi_calculateBounds( inspector, rects, times, renderCount )
				log.warn( inspector.error )
				ASSInspector.assi_cleanup( inspector )
				aegisub.cancel( )

			-- Check out the calculated bounding rects
			for render = 0, renderCount - 1
				logRect( rects[render] )

	-- That's all, folks.
	ASSInspector.assi_cleanup( inspector )

aegisub.register_macro( script_name, script_description, mainFunction )

error: pathspec 'examples/aegisub_example.moon' did not match any file(s) known to git


redditjoke: New module, get jokes from /r/jokes

json = require'json'
simplehttp = require'simplehttp'
math = require'math'

PRIVMSG:
  '^%pjoke': (source, destination, arg) =>
    simplehttp 'http://www.reddit.com/r/jokes.json', (data) ->
      data = json.decode data
      children = data.data.children
      joke = children[math.random 1, #children]
      title = joke.data.title
      joketext = title .. ' ' .. joke.data.selftext
      @Msg 'privmsg', destination, source, joketext

error: pathspec 'modules/redditjoke.moon' did not match any file(s) known to git


k: new module

k: new module

-- k, reimplemented from dbot's k by byte[]
history = {}

PRIVMSG: {
  '^%pk$': (source, destination) =>
    t = {}
    max = 5
    for i = 1, max
      h = history[destination][math.random(#history[destination])]
      msg = h
      want = math.random(2, 4)
      getting = false
      msg = msg\gsub("\003%d?%d?,?%d?%d?", "")
      for w in msg\gmatch("[%w%[%]%{%}%|`%^_%-%|'\194-\244\128-\191]+") do
        if w == "http" or w == "https"
          break
        if not getting
          -- Less odds if not starting with letter.
          if w\find("^[%a']") or math.random(2) == 1
            if math.random(1, max + i + 1) <= i
              getting = true
        if getting
          if want > 0
            t[#t + 1] = w\lower()
          else
            break
          want = want - 1
    outline = table.concat(t, " ")
    @Msg 'privmsg', destination, source, outline
  (source, destination, argument) =>
    unless history[destination]
      history[destination] = {}
    table.insert history[destination], #history[destination]+1, argument
    if #history[destination] > 20
      table.remove history[destination], 1
}

error: pathspec 'modules/k.moon' did not match any file(s) known to git


k: new module

k: new module

Former-commit-id: dda3bb6776e07948f89591774705e878094f5afc [formerly c5b52c36c54968f922c0136f195d683f360e8588]
Former-commit-id: c2493e58eb86bc9fb29e4a21d38bea83bc20294d


Added tests for `split` function

Added tests for `split` function


cubesensors: new module

cubesensors: new module

Former-commit-id: 9cab8fef5e6cd34175a512ff36b5e75368b12cb7 [formerly ed2c9d0afe99fe6c02cfc31df8cd4b6a37233708]
Former-commit-id: 26cab27715e152776ece2e7db4f6e80d3dd88bc6

round = (x) ->
  x + 0.5 - (x + 0.5) % 1

PRIVMSG:
  '^%pinnev.+r$': (source, destination, input) =>
    ivar2.util.simplehttp ivar2.config.cubeapiurl, (data) ->
      result = json.decode data
      if result
        r = result[1]
        temp = r.rtemp
        noise = tonumber(r.noisedba) or 0
        say '\002%.1f\002 °C, \002%s\002 luftkvalitet, \002%s\002 fukt, \002%s\002 lys, \002%s\002 dBA støynivå', temp, round(r.voc), round(r.humidity), round(r.light), noise

error: pathspec 'modules/cubesensors.moon' did not match any file(s) known to git


markov: new module

markov: new module

Former-commit-id: af0dba43e04e8f74dde564d0ae3f1b1411d7f2a5 [formerly 1ad8c69a5c0b391747f0e36db36aa6c0f716aa6d]
Former-commit-id: 930e6a8f7619b6f0f0a2ca7b979a61dfee40e1de

brain = require 'brain' -- https://luarocks.org/modules/darkstalker/brain

-- Cache/Store bots
bots = {}

getbot = (destination) ->
  bot = bots[destination]
  if not bot then
    filename = "cache/markov.#{ivar2.network}.#{destination}.sqlite3"
    bot = brain.new(filename, 1)
    bots[destination] = bot

  return bot

markov = (source, destination, arg) =>
  brain = getbot(destination)
  say brain\reply(arg)

return {
  PRIVMSG: {
    '^%pm (.+)$': markov
    '^%pm$': markov
    (source, destination, arg) =>
      brain = getbot(destination)
      nick = source.nick
      if arg\sub(1,1) == '\001' and arg\sub(-1) == '\001'
        arg = arg\sub(9, -2)

      brain\begin_batch!
      brain\learn arg
      -- Close db (commit)
      brain\end_batch!
  }
}

error: pathspec 'modules/markov.moon' did not match any file(s) known to git


chopra: add the greatness of chopra to the bot

simplehttp = require 'simplehttp'

PRIVMSG:
  '^%pchopra$': (source, destination) =>
    simplehttp 'http://www.wisdomofchopra.com/iframe.php', (html) ->
       quote = html\match [[<meta property="og:description" content="'(.+)' www.wisdomofchopra.com" />]]
       if quote
         say quote

error: pathspec 'modules/chopra.moon' did not match any file(s) known to git


Add a load time configuration

Add a load time configuration

* This still depends upon having a conf.lua file in the base project
  directory, but it goes through the process of handling the Love
  specifics.

require_json = ->
	r, json = pcall require, "dkjson"
	return json if r
	r, json = pcall require, "lua2json"
	return json if r
	r, json = pcall require, "cjson"
	return json if r

-- This should be called from conf.lua from the main directory of your project
-- The filename is actualy very important. Your entire project and LunoPunk
-- must be compiled with moonc before this will work. Love actually reads
-- conf.lua before all others. Using require won't work unless you include the
-- moonscript loader first.
--
-- @param options A table containing options for configuring the engine
-- @param cfg A configuration filename, json is currently accepted. If a pure Lua YAML library is created, that could be an option as well.
-- @param width The window width
-- @param height The window height
-- @param fullscreen If the engine should switch to fullscreen mode
-- @param title The application title
export config = (options) ->
	if options and type(options.cfg) == "string"
		json = require_json!
		if json == nil
			print "Error loading a json decoding library"
		else
			f, err = io.open options.cfg
			if f == nil
				print err
			else
				tmp_options, err = json.decode f\read "*a"
				f\close!
				if tmp_options == nil
					print err
				else
					options[k] = v for k,v in pairs tmp_options

	love.conf = (t) ->
		if options != nil
			t.window.width = options.width if options.width != nil
			t.window.height = options.height if options.height != nil
			t.window.fullscreen = options.fullscreen if options.fullscreen != nil
			t.title = options.title if options.title != nil

error: pathspec 'LunoPunk/Config.moon' did not match any file(s) known to git


weatherunderground: new module

weatherunderground: new module

Former-commit-id: 41bd392b59d304c0cbf429e8ea1097835ffe28f9 [formerly 4ae07b8dece7c3282e9926938302b716c99b761a]
Former-commit-id: 90710803a5e2f284bf3ee763aae122954bb3451e

-- Weather Underground API module, requires a key set in configuration
-- WeatherUndergroundKey

util = require'util'
os = require'os'

key = ivar2.config.WeatherUndergroundKey
unless key
  return {}

geoApiBase = 'http://api.geonames.org/searchJSON?name=%s&featureClass=P&username=haste'
wugeolookupurl = 'http://api.wunderground.com/api/%s/geolookup/q/59.788,5.7218.json'
language = 'NO'

tempColor = (v) ->
  v = tonumber(v)
  if v > 0
    color = util.red
    return color(v)
  else
    color = util.lightblue
    return color(v)

windSpeedToName = (ws) ->
  windtext = ''
  unless ws
    return windtext
  if ws > 32
    windtext = 'orkan!'
  elseif ws > 28.5
    windtext = 'sterk storm'
  elseif ws > 24.5
    windtext = 'full storm'
  elseif ws > 20.8
    windtext = 'liten storm'
  elseif ws > 17.2
    windtext = 'sterk kuling'
  elseif ws > 13.9
    windtext = 'stiv kuling'
  elseif ws > 10.8
    windtext = 'liten kuling'
  elseif ws > 8
    windtext = 'frisk bris'
  elseif ws > 5.5
    windtext = 'laber bris'
  elseif ws > 3.4
    windtext = 'lett bris'
  elseif ws > 1.5
    windtext = 'svak vind'
  elseif ws > 0.3
    windtext = 'flau vind'
  else
    windtext = 'vindstille'
  return windtext

findLocation = (source, destination, name, cb) =>
  gurl = geoApiBase\format(util.urlEncode(name))
  util.simplehttp gurl, (data) ->
    json = util.json.decode(data)
    if json.totalResultsCount == 0
      return @Msg('privmsg', destination, source, "Does that place even exist?")
    city = json.geonames[1]
    lat = city.lat
    lon = city.lng
    cb(lon, lat)

lookupConditions = (source, destination, input, pws) =>
  findLocation @, source, destination, input, (lon, lat) ->
    pws = pws or false
    if pws
      pws = 'pws:1/'
    else
      pws = 'pws:0/'
    url = "http://api.wunderground.com/api/#{key}/lang:#{language}/#{pws}conditions/q/#{lat},#{lon}.json"
    @Log 'debug', "Fetching WU URL: %s", url
    util.simplehttp url, (data) ->
      json = util.json.decode(data)
      if json.error ~= nil and json.error.description ~=nil
        return say json.error.description
      current = json.current_observation

      location = current.observation_location
      city = location.city

      weather = current.weather
      temp = current.temp_c
      feelsLike = current.feelslike_c

      windSpeedname = windSpeedToName(current.wind_kph/3.6)

      windDirection = current.wind_dir

      rain = current.precip_today_metric

      time = os.date('%H:%M', tonumber(current.observation_epoch))

      @Msg 'privmsg', destination, source, '%s, %s °C (følt %s °C), %s %s, %s mm, (%s, %s)', weather, tempColor(temp), tempColor(feelsLike), windDirection, windSpeedname, rain, city, time

lookupConditionsPWS = (source, destination, input) =>
  lookupConditions(@, source, destination, input, true)

PRIVMSG:
  '^%pwu (.+)$': lookupConditions
  '^%ppws (.+)$': lookupConditionsPWS

error: pathspec 'modules/weatherunderground.moon' did not match any file(s) known to git


autoop: new module, autoop owners and chan owners

verifyOwner = (src) ->
  for _,mask in pairs(ivar2.config.owners)
    if src.mask\match(mask)
      return true

verifyChannelOwner = (src, destination) ->
  channel = ivar2.config.channels[destination]

  if type(channel) == 'table' and type(channel.owners) == 'table'
    for _,mask in pairs(channel.owners)
      if src.mask\match(mask)
        return true

JOIN: {
  (source, destination, arg) =>
    if verifyOwner source
      @Log 'info', 'Automatically OPing owner'
      @Mode destination, "+o #{source.nick}"
    elseif verifyChannelOwner source, destination
      @Log 'info', 'Automatically OPing channel owner'
      @Mode destination, "+o #{source.nick}"

}

error: pathspec 'modules/autoop.moon' did not match any file(s) known to git


bobross: new module

bobross: new module

util = require'util'

data = [[

[
  "That's a crooked tree. We'll send him to Washington.",
  "The only thing worse than yellow snow is green snow.",
  "I like to beat the brush.",
  "In painting, you have unlimited power. You have the ability to move mountains. You can bend rivers. But when I get home, the only thing I have power over is the garbage.",
  "You need the dark in order to show the light.",
  "Look around. Look at what we have. Beauty is everywhere you only have to look to see it.",
  "Just go out and talk to a tree. Make friends with it.",
  "There's nothing wrong with having a tree as a friend.",
  "Trees cover up a multitude of sins.",
  "They say everything looks better with odd numbers of things. But sometimes I put even numbers\u00e2\u20ac\u201djust to upset the critics.",
  "How do you make a round circle with a square knife? That's your challenge for the day.",
  "A thin paint will stick to a thick paint.",
  "Just beat the devil out of it.",
  "We don't make mistakes we just have happy little accidents.",
  "We'll paint one happy little tree right here.",
  "Just pretend you are a whisper floating across a mountain.",
  "From all of us here, I want to wish you happy painting and God bless, my friends.",
  "We tell people sometimes: we're like drug dealers, come into town and get everybody absolutely addicted to painting. It doesn't take much to get you addicted.",
  "The secret to doing anything is believing that you can do it. Anything that you believe you can do strong enough, you can do. Anything. As long as you believe.",
  "Water's like me. It's laaazy\u00c2 ... Boy, it always looks for the easiest way to do things",
  "I really believe that if you practice enough you could paint the 'Mona Lisa' with a two-inch brush.",
  "If I paint something, I don't want to have to explain what it is.",
  "We artists are a different breed of people. We're a happy bunch.",
  "I guess I'm a little weird. I like to talk to trees and animals. That's okay though; I have more fun than most people.",
  "Let's get crazy.",
  "I can't think of anything more rewarding than being able to express yourself to others through painting.",
  "Exercising the imagination, experimenting with talents, being creative; these things, to me, are truly the windows to your soul.",
  "All you need to paint is a few tools, a little instruction, and a vision in your mind.",
  "I started painting as a hobby when I was little. I didn't know I had any talent. I believe talent is just a pursued interest. Anybody can do what I do.",
  "Everyone is going to see things differently - and that's the way it should be.",
  "No pressure. Just relax and watch it happen.",
  "When you do it your way you can go anywhere you choose.",
  "You can create beautiful things - but you have to see them in your mind first",
  "It's so important to do something every day that will make you happy",
  "Don't be afraid to make these big decisions. Once you start, they sort of just make themselves.",
  "With something so strong, a little bit can go a long way.",
  "You have to allow the paint to break to make it beautiful.",
  "Think about a cloud. Just float around and be there.",
  "In nature, dead trees are just as normal as live trees.",
  "It's hard to see things when you're too close. Take a step back and look.",
  "Only think about one thing at a time. Don't get greedy.",
  "If you do too much it's going to lose its effectiveness.",
  "Use absolutely no pressure. Just like an angel's wing.",
  "You're meant to have fun in life.",
  "In life you need colors.",
  "It's a super day, so why not make a beautiful sky?",
  "Just let go - and fall like a little waterfall.",
  "It's beautiful - and we haven't even done anything to it yet.",
  "Be careful. You can always add more - but you can't take it away.",
  "Pretend you're water. Just floating without any effort. Having a good day.",
  "When things happen - enjoy them. They're little gifts.",
  "Take your time. Speed will come later.",
  "It's amazing what you can do with a little love in your heart.",
  "God gave you this gift of imagination. Use it.",
  "Paint anything you want on the canvas. Create your own world.",
  "That's what painting is all about. It should make you feel good when you paint.",
  "You can do anything your heart can imagine.",
  "With practice comes confidence.",
  "This is happy place, little squirrels live here and play.",
  "We have no limits to our world. We're only limited by our imagination.",
  "Be so very light. Be a gentle whisper.",
  "The least little bit can do so much.",
  "All you have to do is let your imagination go wild.",
  "Didn't you know you had that much power? You can move mountains. You can do anything.",
  "If you don't like it - change it. It's your world.",
  "There is immense joy in just watching - watching all the little creatures in nature.",
  "Don't forget to tell these special people in your life just how special they are to you.",
  "That is when you can experience true joy, when you have no fear.",
  "We don't really know where this goes - and I'm not sure we really care.",
  "Life is too short to be alone, too precious. Share it with a friend.",
  "Work on one thing at a time. Don't get carried away - we have plenty of time.",
  "If we're going to have animals around we all have to be concerned about them and take care of them.",
  "Almost everything is going to happen for you automatically - you don't have to spend any time working or worrying.",
  "Sometimes you learn more from your mistakes than you do from your masterpieces.",
  "There are no limits in this world.",
  "You can create anything that makes you happy.",
  "You want your tree to have some character. Make it special.",
  "In your world you have total and absolute power.",
  "The man who does the best job is the one who is happy at his job.",
  "Everything's not great in life, but we can still find beauty in it.",
  "Don't hurry. Take your time and enjoy.",
  "You're the greatest thing that has ever been or ever will be. You're special. You're so very special.",
  "There is no right or wrong - as long as it makes you happy and doesn't hurt anyone.",
  "Everyone needs a friend. Friends are the most valuable things in the world.",
  "There are no mistakes. You can fix anything that happens.",
  "That's why I paint - because I can create the kind of world I want - and I can make this world as happy as I want it.",
  "It's life. It's interesting. It's fun.",
  "Just think about these things in your mind - then bring them into your world.",
  "In your imagination you can go anywhere you want.",
  "That's what makes life fun. That you can make these decisions. That you can create the world that you want.",
  "Go out on a limb - that's where the fruit is.",
  "In this world, everything can be happy.",
  "All you have to learn here is how to have fun.",
  "Isn't it great to do something you can't fail at?",
  "Anytime you learn something your time and energy are not wasted.",
  "A tree cannot be straight if it has a crooked trunk.",
  "You've got to learn to fight the temptation to resist these things. Just let them happen.",
  "You create the dream - then you bring it into your world.",
  "These things happen automatically. All you have to do is just let them happen.",
  "You can do anything here. So don't worry about it.",
  "This present moment is perfect simply due to the fact you're experiencing it.",
  "Only God can make a tree - but you can paint one.",
  "You can't make a mistake. Anything that happens you can learn to use - and make something beautiful out of it.",
  "Put light against light - you have nothing. Put dark against dark - you have nothing. It's the contrast of light and dark that each give the other one meaning.",
  "This is an example of what you can do with just a few things, a little imagination and a happy dream in your heart.",
  "If what you're doing doesn't make you happy - you're doing the wrong thing.",
  "Just relax and let it flow. That easy.",
  "Every single thing in the world has its own personality - and it is up to you to make friends with the little rascals.",
  "Trees grow however makes them happy.",
  "Don't kill all your dark areas - you need them to show the light.",
  "Everyone wants to enjoy the good parts - but you have to build the framework first.",
  "A big strong tree needs big strong roots.",
  "The light is your friend. Preserve it.",
  "Everybody's different. Trees are different. Let them all be individuals.",
  "We want to use a lot pressure while using no pressure at all.",
  "Follow the lay of the land. It's most important.",
  "We don't have anything but happy trees here.",
  "Even trees need a friend. We all need friends.",
  "Isn't it fantastic that you can change your mind and create all these happy things?",
  "The first step to doing anything is to believe you can do it. See it finished in your mind before you ever start.",
  "No worries. No cares. Just float and wait for the wind to blow you around.",
  "It just happens - whether or not you worried about it or tried to plan it.",
  "If it's not what you want - stop and change it. Don't just keep going and expect it will get better.",
  "You can create the world you want to see and be a part of. You have that power.",
  "How to paint. That's easy. What to paint. That's much harder.",
  "We can always carry this a step further. There's really no end to this.",
  "But we're not there yet, so we don't need to worry about it.",
  "This is your creation - and it's just as unique and special as you are.",
  "La- da- da- da- dah. Just be happy.",
  "Just take out whatever you don't want. It'll change your entire perspective.",
  "If you don't think every day is a good day - try missing a few. You'll see.",
  "There isn't a rule. You just practice and find out which way works best for you.",
  "Just let your mind wander and enjoy. This should make you happy.",
  "We don't have to be committed. We are just playing here.",
  "Remember how free clouds are. They just lay around in the sky all day long.",
  "Any little thing can be your friend if you let it be.",
  "The very fact that you're aware of suffering is enough reason to be overjoyed that you're alive and can experience it.",
  "And that's when it becomes fun - you don't have to spend your time thinking about what's happening - you just let it happen.",
  "This is probably the greatest thing to happen in my life - to be able to share this with you.",
  "If there are two big trees, eventually there will be a little tree.",
  "Everybody needs a friend.",
  "You can do anything here - the only pre-requisite is that it makes you happy.",
  "The more we do this - the more it will do good things to our heart.",
  "Even the worst thing we can do here is good.",
  "Absolutely no pressure. You are just a whisper floating across a mountain.",
  "You have freedom here. The only guide is your heart.",
  "Trees grow in all kinds of ways. They're not all perfectly straight. Not every limb is perfect.",
  "We must be quiet, soft and gentle.",
  "You can't have light without dark. You can't know happiness unless you've known sorrow.",
  "Just let this happen. We just let this flow right out of our minds.",
  "Just make a decision and let it go.",
  "Everything is happy if you choose to make it that way.",
  "You could sit here for weeks with your one hair brush trying to do that - or you could do it with one stroke with an almighty brush.",
  "We're not trying to teach you a thing to copy. We're just here to teach you a technique, then let you loose into the world.",
  "A tree needs to be your friend if you're going to paint him.",
  "You have to make almighty decisions when you're the creator.",
  "This is your world, whatever makes you happy you can put in it. Go crazy.",
  "We don't have to be concerned about it. We just have to let it fall where it will.",
  "We don't need any guidelines or formats. All we need to do is just let it flow right out of us.",
  "Let all these little things happen. Don't fight them. Learn to use them.",
  "I sincerely wish for you every possible joy life could bring.",
  "We spend so much of our life looking - but never seeing.",
  "Talent is a pursued interest. That is to say, anything you practice you can do.",
  "Let's make some happy little clouds in our world"
]
]]

PRIVMSG:
  '^%pbobross$': =>
    quotes = util.json.decode data
    choice = math.random(1, #quotes)
    say quotes[choice]

error: pathspec 'modules/bobross.moon' did not match any file(s) known to git


trigger: first version

trigger: first version

sql = require'lsqlite3'

openDb = ->
  db = sql.open 'cache/trigger.sql'

  db\exec [[
    CREATE TABLE IF NOT EXISTS trigger (
      pattern UNIQUE ON CONFLICT REPLACE,
      funcstr
    );
    ]]
  return db

triggerHelp  = (source, destination, argument) =>
   @Msg 'privmsg', source, destination, 'Usage: !trigger add <lua pattern>|<lua code>. Ex: !trigger add !help (%w+)|say "%s, you need help'

-- Construct a safe environ for trigger with two functions; say and reply
--- TODO: actually make it safe :-)
--- Preferrably you should be able to cross-call modules here to make aliasing possible
sandbox = (func) ->
  (source, destination, ...) =>
    -- Store args so we can use them in env
    arg = {...}
    env =
      say: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      reply: (str) ->
        @Msg 'privmsg', destination, source, source.nick..': '..str, unpack(arg),
      print: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      simplehttp:require'simplehttp'
      json:require'json'
      :string
      :type
      :tostring
      :tonumber
      :ipars
      :pairs
      :table
      :next

    --setfenv func, setmetatable(env, {__index: _G })
    setfenv func, env
    success, err = pcall func, arg
    if err
      @Msg 'privmsg', destination, source, err
    else
      success

-- Construct a safe function to run and return a handler
triggerHandler = (source, destination, funcstr) =>
  func, err = loadstring funcstr
  unless func
    @Msg 'privmsg', destination, source, 'Trigger error: %s', err
  else
    sandbox(func)

-- Stub to create a trigger name in event table
handlerName = (pattern) ->
  "trigger#{pattern}"

-- Register the command
regCommand = (source, destination, pattern, funcstr) =>
  db = openDb!
  ins = db\prepare "INSERT INTO trigger (pattern, funcstr) VALUES(?, ?)"
  code = ins\bind_values pattern, funcstr
  code = ins\step!
  code = ins\finalize!
  db\close!

  @RegisterCommand handlerName(pattern), pattern, triggerHandler(@, source, destination, funcstr)

delCommand = (source, destination, pattern) =>
  db = openDb!
  ins = db\prepare "DELETE FROM trigger WHERE pattern = ?"
  code = ins\bind_values pattern
  code = ins\step!
  code = ins\finalize!
  db\close!
  @UnregisterCommand handlerName(pattern)

-- Register commands on startup
db = openDb!
for row in db\nrows 'SELECT pattern, funcstr FROM trigger'
  print ivar2
  ivar2\RegisterCommand handlerName(row.pattern), row.pattern, triggerHandler(ivar2, nil, nil, row.funcstr)

PRIVMSG:
  '%ptrigger$': triggerHelp
  '%ptrigger add (.+)|(.+)$': regCommand
  '%ptrigger del (.+)$': delCommand

error: pathspec 'modules/trigger.moon' did not match any file(s) known to git