Implemented a SocialManager with support for posting to both Twitter and Mastodon

This commit is contained in:
Erik C. Thauvin 2022-12-05 23:50:16 -08:00
parent 6a3caa84ce
commit 6fd59e9487
42 changed files with 642 additions and 269 deletions

View file

@ -57,6 +57,10 @@ jobs:
TWITTER_HANDLE: ${{ secrets.TWITTER_HANDLE }} TWITTER_HANDLE: ${{ secrets.TWITTER_HANDLE }}
TWITTER_TOKEN: ${{ secrets.TWITTER_TOKEN }} TWITTER_TOKEN: ${{ secrets.TWITTER_TOKEN }}
TWITTER_TOKENSECRET: ${{ secrets.TWITTER_TOKENSECRET }} TWITTER_TOKENSECRET: ${{ secrets.TWITTER_TOKENSECRET }}
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
MASTODON_HANDLE: ${{ secrets.MASTODON_HANDLE }}
MASTODON_INSTANCE: ${{ secrets.MASTODON_INSTANCE }}
run: ./gradlew build check --stacktrace run: ./gradlew build check --stacktrace
- name: SonarCloud - name: SonarCloud

View file

@ -2,9 +2,9 @@
<SmellBaseline> <SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues> <ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues> <CurrentIssues>
<ID>CyclomaticComplexMethod:FeedsMgr.kt$FeedsMgr.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID> <ID>CyclomaticComplexMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>CyclomaticComplexMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>CyclomaticComplexMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>LongMethod:FeedsMgr.kt$FeedsMgr.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID> <ID>LongMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>LongMethod:Mobibot.kt$Mobibot.Companion$@JvmStatic @Throws(Exception::class) fun main(args: Array&lt;String&gt;)</ID> <ID>LongMethod:Mobibot.kt$Mobibot.Companion$@JvmStatic @Throws(Exception::class) fun main(args: Array&lt;String&gt;)</ID>
<ID>LongMethod:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>LongMethod:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>LongMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>LongMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>
@ -23,13 +23,14 @@
<ID>MagicNumber:Info.kt$Info.Companion$30</ID> <ID>MagicNumber:Info.kt$Info.Companion$30</ID>
<ID>MagicNumber:Info.kt$Info.Companion$365</ID> <ID>MagicNumber:Info.kt$Info.Companion$365</ID>
<ID>MagicNumber:Info.kt$Info.Companion$7</ID> <ID>MagicNumber:Info.kt$Info.Companion$7</ID>
<ID>MagicNumber:Mastodon.kt$Mastodon.Companion$200</ID>
<ID>MagicNumber:Mobibot.kt$Mobibot$8</ID> <ID>MagicNumber:Mobibot.kt$Mobibot$8</ID>
<ID>MagicNumber:Modules.kt$Modules$7</ID> <ID>MagicNumber:Modules.kt$Modules$7</ID>
<ID>MagicNumber:SocialManager.kt$SocialManager$1000L</ID>
<ID>MagicNumber:SocialManager.kt$SocialManager$60L</ID>
<ID>MagicNumber:StockQuote.kt$StockQuote.Companion$10</ID> <ID>MagicNumber:StockQuote.kt$StockQuote.Companion$10</ID>
<ID>MagicNumber:Tell.kt$Tell$50</ID> <ID>MagicNumber:Tell.kt$Tell$50</ID>
<ID>MagicNumber:Tell.kt$Tell$7</ID> <ID>MagicNumber:Tell.kt$Tell$7</ID>
<ID>MagicNumber:Twitter.kt$Twitter$1000L</ID>
<ID>MagicNumber:Twitter.kt$Twitter$60L</ID>
<ID>MagicNumber:TwitterOAuth.kt$TwitterOAuth$401</ID> <ID>MagicNumber:TwitterOAuth.kt$TwitterOAuth$401</ID>
<ID>MagicNumber:Users.kt$Users$8</ID> <ID>MagicNumber:Users.kt$Users$8</ID>
<ID>MagicNumber:Utils.kt$Utils$200</ID> <ID>MagicNumber:Utils.kt$Utils$200</ID>
@ -45,18 +46,19 @@
<ID>MagicNumber:WorldTime.kt$WorldTime.Companion$60</ID> <ID>MagicNumber:WorldTime.kt$WorldTime.Companion$60</ID>
<ID>MagicNumber:WorldTime.kt$WorldTime.Companion$86.4</ID> <ID>MagicNumber:WorldTime.kt$WorldTime.Companion$86.4</ID>
<ID>MaxLineLength:TwitterOAuth.kt$TwitterOAuth$*</ID> <ID>MaxLineLength:TwitterOAuth.kt$TwitterOAuth$*</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(command: AbstractCommand)</ID> <ID>NestedBlockDepth:Addons.kt$Addons$fun add(command: AbstractCommand): Boolean</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule)</ID> <ID>NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule): Boolean</ID>
<ID>NestedBlockDepth:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?): String</ID> <ID>NestedBlockDepth:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?): String</ID>
<ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID> <ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(query: String): Message</ID> <ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(query: String): Message</ID>
<ID>NestedBlockDepth:EntryLink.kt$EntryLink$private fun setTags(tags: List&lt;String?&gt;)</ID> <ID>NestedBlockDepth:EntryLink.kt$EntryLink$private fun setTags(tags: List&lt;String?&gt;)</ID>
<ID>NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$@JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = currentXml): String</ID> <ID>NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = currentXml): String</ID>
<ID>NestedBlockDepth:FeedsMgr.kt$FeedsMgr.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID> <ID>NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = currentXml)</ID>
<ID>NestedBlockDepth:GoogleSearch.kt$GoogleSearch$override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent)</ID> <ID>NestedBlockDepth:GoogleSearch.kt$GoogleSearch$override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List&lt;Message&gt;</ID> <ID>NestedBlockDepth:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List&lt;Message&gt;</ID>
<ID>NestedBlockDepth:LinksMgr.kt$LinksMgr$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID> <ID>NestedBlockDepth:LinksManager.kt$LinksManager$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Lookup.kt$Lookup$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent)</ID> <ID>NestedBlockDepth:Lookup.kt$Lookup$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Mastodon.kt$Mastodon.Companion$@JvmStatic @Throws(ModuleException::class) fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String</ID>
<ID>NestedBlockDepth:Posting.kt$Posting$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID> <ID>NestedBlockDepth:Posting.kt$Posting$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:Seen.kt$Seen$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID> <ID>NestedBlockDepth:Seen.kt$Seen$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>NestedBlockDepth:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
@ -77,6 +79,7 @@
<ID>ThrowsCount:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?): String</ID> <ID>ThrowsCount:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?): String</ID>
<ID>ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List&lt;Message&gt;</ID> <ID>ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List&lt;Message&gt;</ID>
<ID>ThrowsCount:Joke.kt$Joke.Companion$@JvmStatic @Throws(ModuleException::class) fun randomJoke(): List&lt;Message&gt;</ID> <ID>ThrowsCount:Joke.kt$Joke.Companion$@JvmStatic @Throws(ModuleException::class) fun randomJoke(): List&lt;Message&gt;</ID>
<ID>ThrowsCount:Mastodon.kt$Mastodon.Companion$@JvmStatic @Throws(ModuleException::class) fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject</ID> <ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject</ID>
<ID>ThrowsCount:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID> <ID>ThrowsCount:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List&lt;Message&gt;</ID>

View file

@ -41,12 +41,25 @@ tell-max-size=50
#twitter-token= #twitter-token=
#twitter-tokenSecret= #twitter-tokenSecret=
# Twitter handle to receive channel join notifications # Twitter handle to receive channel join/leave notifications
#twitter-handle= #twitter-handle=
# Automatically post links to twitter # Automatically post links to Mastodon
#twitter-auto-post=true #twitter-auto-post=true
#
# Create a Mastodon application access token at: https//SERVER_INSTANCE/settings/applications
# Make sure the 'write:statuses' scope is enabled.
#
#mastodon-access-token=
#mastodon-instance=mastodon.social
# Mastodon handle to receive channel join/leave notifications
#mastodon-handle=@mobitopia
# Automatically post links to Mastodon
#mastodon-auto-post=true
# #
# Create custom search engine at: https://cse.google.com/ # Create custom search engine at: https://cse.google.com/
# and get API key from: https://console.developers.google.com/ # and get API key from: https://console.developers.google.com/
@ -65,7 +78,7 @@ tell-max-size=50
#alphavantage-api-key= #alphavantage-api-key=
# #
# Get Wolfram Alpa AppID from: https://developer.wolframalpha.com/portal/ # Get Wolfram Alpha AppID from: https://developer.wolframalpha.com/portal/
# #
#wolfram-appid= #wolfram-appid=
#wolfram-units=imperial #wolfram-units=imperial

View file

@ -33,7 +33,7 @@ package net.thauvin.erik.mobibot
import net.thauvin.erik.mobibot.Utils.notContains import net.thauvin.erik.mobibot.Utils.notContains
import net.thauvin.erik.mobibot.commands.AbstractCommand import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.commands.links.LinksMgr import net.thauvin.erik.mobibot.commands.links.LinksManager
import net.thauvin.erik.mobibot.modules.AbstractModule import net.thauvin.erik.mobibot.modules.AbstractModule
import org.pircbotx.hooks.events.PrivateMessageEvent import org.pircbotx.hooks.events.PrivateMessageEvent
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
@ -46,8 +46,8 @@ import java.util.Properties
*/ */
class Addons(private val props: Properties) { class Addons(private val props: Properties) {
private val logger: Logger = LoggerFactory.getLogger(Addons::class.java) private val logger: Logger = LoggerFactory.getLogger(Addons::class.java)
private val disabledModules = props.getProperty("disabled-modules", "").split(LinksMgr.TAG_MATCH.toRegex()) private val disabledModules = props.getProperty("disabled-modules", "").split(LinksManager.TAG_MATCH.toRegex())
private val disableCommands = props.getProperty("disabled-commands", "").split(LinksMgr.TAG_MATCH.toRegex()) private val disableCommands = props.getProperty("disabled-commands", "").split(LinksManager.TAG_MATCH.toRegex())
val commands: MutableList<AbstractCommand> = mutableListOf() val commands: MutableList<AbstractCommand> = mutableListOf()
val modules: MutableList<AbstractModule> = mutableListOf() val modules: MutableList<AbstractModule> = mutableListOf()
@ -56,7 +56,8 @@ class Addons(private val props: Properties) {
/** /**
* Add a module with properties. * Add a module with properties.
*/ */
fun add(module: AbstractModule) { fun add(module: AbstractModule): Boolean {
var enabled = false
with(module) { with(module) {
if (disabledModules.notContains(name, true)) { if (disabledModules.notContains(name, true)) {
if (hasProperties()) { if (hasProperties()) {
@ -69,6 +70,7 @@ class Addons(private val props: Properties) {
modules.add(this) modules.add(this)
names.modules.add(name) names.modules.add(name)
names.commands.addAll(commands) names.commands.addAll(commands)
enabled = true
} else { } else {
if (logger.isDebugEnabled) { if (logger.isDebugEnabled) {
logger.debug("Module $name is disabled.") logger.debug("Module $name is disabled.")
@ -76,12 +78,14 @@ class Addons(private val props: Properties) {
} }
} }
} }
return enabled
} }
/** /**
* Add a command with properties. * Add a command with properties.
*/ */
fun add(command: AbstractCommand) { fun add(command: AbstractCommand): Boolean {
var enabled = false
with(command) { with(command) {
if (disableCommands.notContains(name, true)) { if (disableCommands.notContains(name, true)) {
if (properties.isNotEmpty()) { if (properties.isNotEmpty()) {
@ -98,6 +102,7 @@ class Addons(private val props: Properties) {
names.commands.add(name) names.commands.add(name)
} }
} }
enabled = true
} else { } else {
if (logger.isDebugEnabled) { if (logger.isDebugEnabled) {
logger.debug("Command $name is disabled.") logger.debug("Command $name is disabled.")
@ -105,6 +110,7 @@ class Addons(private val props: Properties) {
} }
} }
} }
return enabled
} }
/** /**

View file

@ -37,7 +37,7 @@ import com.rometools.rome.io.XmlReader
import net.thauvin.erik.mobibot.Utils.green import net.thauvin.erik.mobibot.Utils.green
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.sendMessage import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.entries.FeedsMgr import net.thauvin.erik.mobibot.entries.FeedsManager
import net.thauvin.erik.mobibot.msg.Message import net.thauvin.erik.mobibot.msg.Message
import net.thauvin.erik.mobibot.msg.NoticeMessage import net.thauvin.erik.mobibot.msg.NoticeMessage
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
@ -50,7 +50,7 @@ import java.net.URL
* Reads an RSS feed. * Reads an RSS feed.
*/ */
class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable { class FeedReader(private val url: String, val event: GenericMessageEvent) : Runnable {
private val logger: Logger = LoggerFactory.getLogger(FeedsMgr::class.java) private val logger: Logger = LoggerFactory.getLogger(FeedsManager::class.java)
/** /**
* Fetches the Feed's items. * Fetches the Feed's items.

View file

@ -61,7 +61,7 @@ import net.thauvin.erik.mobibot.commands.Say
import net.thauvin.erik.mobibot.commands.Users import net.thauvin.erik.mobibot.commands.Users
import net.thauvin.erik.mobibot.commands.Versions import net.thauvin.erik.mobibot.commands.Versions
import net.thauvin.erik.mobibot.commands.links.Comment import net.thauvin.erik.mobibot.commands.links.Comment
import net.thauvin.erik.mobibot.commands.links.LinksMgr import net.thauvin.erik.mobibot.commands.links.LinksManager
import net.thauvin.erik.mobibot.commands.links.Posting import net.thauvin.erik.mobibot.commands.links.Posting
import net.thauvin.erik.mobibot.commands.links.Tags import net.thauvin.erik.mobibot.commands.links.Tags
import net.thauvin.erik.mobibot.commands.links.View import net.thauvin.erik.mobibot.commands.links.View
@ -75,9 +75,11 @@ import net.thauvin.erik.mobibot.modules.Dice
import net.thauvin.erik.mobibot.modules.GoogleSearch import net.thauvin.erik.mobibot.modules.GoogleSearch
import net.thauvin.erik.mobibot.modules.Joke import net.thauvin.erik.mobibot.modules.Joke
import net.thauvin.erik.mobibot.modules.Lookup import net.thauvin.erik.mobibot.modules.Lookup
import net.thauvin.erik.mobibot.modules.Mastodon
import net.thauvin.erik.mobibot.modules.Ping import net.thauvin.erik.mobibot.modules.Ping
import net.thauvin.erik.mobibot.modules.RockPaperScissors import net.thauvin.erik.mobibot.modules.RockPaperScissors
import net.thauvin.erik.mobibot.modules.StockQuote import net.thauvin.erik.mobibot.modules.StockQuote
import net.thauvin.erik.mobibot.modules.Twitter
import net.thauvin.erik.mobibot.modules.War import net.thauvin.erik.mobibot.modules.War
import net.thauvin.erik.mobibot.modules.Weather2 import net.thauvin.erik.mobibot.modules.Weather2
import net.thauvin.erik.mobibot.modules.WolframAlpha import net.thauvin.erik.mobibot.modules.WolframAlpha
@ -146,7 +148,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
) )
event.sendMessage("The commands are:") event.sendMessage("The commands are:")
event.sendList(addons.names.commands, 8, isBold = true, isIndent = true) event.sendList(addons.names.commands, 8, isBold = true, isIndent = true)
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
event.sendMessage("The op commands are:") event.sendMessage("The op commands are:")
event.sendList(addons.names.ops, 8, isBold = true, isIndent = true) event.sendList(addons.names.ops, 8, isBold = true, isIndent = true)
} }
@ -174,11 +176,11 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
override fun onDisconnect(event: DisconnectEvent?) { override fun onDisconnect(event: DisconnectEvent?) {
event?.let { event?.let {
with(event.getBot<PircBotX>()) { with(event.getBot<PircBotX>()) {
LinksMgr.twitter.notification("$nick disconnected from irc://$serverHostname") LinksManager.socialManager.notification("$nick disconnected from irc://$serverHostname")
seen.add(userChannelDao.getChannel(channel).users) seen.add(userChannelDao.getChannel(channel).users)
} }
} }
LinksMgr.twitter.shutdown() LinksManager.socialManager.shutdown()
} }
override fun onPrivateMessage(event: PrivateMessageEvent?) { override fun onPrivateMessage(event: PrivateMessageEvent?) {
@ -199,7 +201,9 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
event?.user?.let { user -> event?.user?.let { user ->
with(event.getBot<PircBotX>()) { with(event.getBot<PircBotX>()) {
if (user.nick == nick) { if (user.nick == nick) {
LinksMgr.twitter.notification("$nick has joined ${event.channel.name} on irc://$serverHostname") LinksManager.socialManager.notification(
"$nick has joined ${event.channel.name} on irc://$serverHostname"
)
seen.add(userChannelDao.getChannel(channel).users) seen.add(userChannelDao.getChannel(channel).users)
} else { } else {
tell.send(event) tell.send(event)
@ -247,7 +251,9 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
event?.user?.let { user -> event?.user?.let { user ->
with(event.getBot<PircBotX>()) { with(event.getBot<PircBotX>()) {
if (user.nick == nick) { if (user.nick == nick) {
LinksMgr.twitter.notification("$nick has left ${event.channel.name} on irc://$serverHostname") LinksManager.socialManager.notification(
"$nick has left ${event.channel.name} on irc://$serverHostname"
)
seen.add(userChannelDao.getChannel(channel).users) seen.add(userChannelDao.getChannel(channel).users)
} else { } else {
seen.add(user.nick) seen.add(user.nick)
@ -388,7 +394,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
}.buildConfiguration() }.buildConfiguration()
// Load the current entries // Load the current entries
with(LinksMgr) { with(LinksManager) {
entries.channel = channel entries.channel = channel
entries.ircServer = ircServer entries.ircServer = ircServer
entries.logsDir = logsDirPath entries.logsDir = logsDirPath
@ -407,7 +413,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
addons.add(Cycle()) addons.add(Cycle())
addons.add(Die()) addons.add(Die())
addons.add(Ignore()) addons.add(Ignore())
addons.add(LinksMgr()) addons.add(LinksManager())
addons.add(Me()) addons.add(Me())
addons.add(Modules(addons.names.modules)) addons.add(Modules(addons.names.modules))
addons.add(Msg()) addons.add(Msg())
@ -426,11 +432,13 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro
tell = Tell("${logsDirPath}${nickname}.ser") tell = Tell("${logsDirPath}${nickname}.ser")
addons.add(tell) addons.add(tell)
addons.add(LinksMgr.twitter)
addons.add(Users()) addons.add(Users())
addons.add(Versions()) addons.add(Versions())
addons.add(View()) addons.add(View())
// Load social modules
LinksManager.socialManager.add(addons, Twitter(), Mastodon())
// Load the modules // Load the modules
addons.add(Calc()) addons.add(Calc())
addons.add(ChatGpt()) addons.add(ChatGpt())

View file

@ -65,6 +65,18 @@ import kotlin.io.path.fileSize
object Utils { object Utils {
private val searchFlags = arrayOf("%c", "%n") private val searchFlags = arrayOf("%c", "%n")
/**
* Prepends a prefix if not present.
*/
@JvmStatic
fun String.prefixIfMissing(prefix: Char): String {
return if (first() != prefix) {
"$prefix${this}"
} else {
this
}
}
/** /**
* Appends a suffix to the end of the String if not present. * Appends a suffix to the end of the String if not present.
*/ */
@ -183,8 +195,8 @@ object Utils {
* Returns {@code true} if the specified user is an operator on the [channel]. * Returns {@code true} if the specified user is an operator on the [channel].
*/ */
@JvmStatic @JvmStatic
fun isChannelOp(channel: String, event: GenericMessageEvent): Boolean { fun GenericMessageEvent.isChannelOp(channel: String): Boolean {
return event.bot().userChannelDao.getChannel(channel).isOp(event.user) return this.bot().userChannelDao.getChannel(channel).isOp(this.user)
} }
/** /**

View file

@ -51,7 +51,7 @@ abstract class AbstractCommand {
abstract fun commandResponse(channel: String, args: String, event: GenericMessageEvent) abstract fun commandResponse(channel: String, args: String, event: GenericMessageEvent)
open fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean { open fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
if (!isOpOnly || isOpOnly == isChannelOp(channel, event)) { if (!isOpOnly || isOpOnly == event.isChannelOp(channel)) {
for (h in help) { for (h in help) {
event.sendMessage(helpCmdSyntax(h, event.bot().nick, event is PrivateMessageEvent || !isPublic)) event.sendMessage(helpCmdSyntax(h, event.bot().nick, event is PrivateMessageEvent || !isPublic))
} }

View file

@ -49,7 +49,7 @@ class Cycle : AbstractCommand() {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
with(event.bot()) { with(event.bot()) {
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
runBlocking { runBlocking {
sendIRC().message(channel, "${event.user.nick} asked me to leave. I'll be back!") sendIRC().message(channel, "${event.user.nick} asked me to leave. I'll be back!")
userChannelDao.getChannel(channel).send().part() userChannelDao.getChannel(channel).send().part()

View file

@ -45,7 +45,7 @@ class Die : AbstractCommand() {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
with(event.bot()) { with(event.bot()) {
if (isChannelOp(channel, event) && (properties[DIE_PROP].isNullOrBlank() || args == properties[DIE_PROP])) { if (event.isChannelOp(channel) && (properties[DIE_PROP].isNullOrBlank() || args == properties[DIE_PROP])) {
sendIRC().message(channel, "${event.user?.nick} has just signed my death sentence.") sendIRC().message(channel, "${event.user?.nick} has just signed my death sentence.")
stopBotReconnect() stopBotReconnect()
sendIRC().quitServer("The Bot is Out There!") sendIRC().quitServer("The Bot is Out There!")

View file

@ -39,7 +39,7 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp import net.thauvin.erik.mobibot.Utils.isChannelOp
import net.thauvin.erik.mobibot.Utils.sendList import net.thauvin.erik.mobibot.Utils.sendList
import net.thauvin.erik.mobibot.Utils.sendMessage import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.links.LinksMgr import net.thauvin.erik.mobibot.commands.links.LinksManager
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
class Ignore : AbstractCommand() { class Ignore : AbstractCommand() {
@ -79,7 +79,7 @@ class Ignore : AbstractCommand() {
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
val isMe = args.trim().equals(me, true) val isMe = args.trim().equals(me, true)
if (isMe || !isChannelOp(channel, event)) { if (isMe || !event.isChannelOp(channel)) {
val nick = event.user.nick.lowercase() val nick = event.user.nick.lowercase()
ignoreNick(nick, isMe, event) ignoreNick(nick, isMe, event)
} else { } else {
@ -88,7 +88,7 @@ class Ignore : AbstractCommand() {
} }
override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean { override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
return if (isChannelOp(channel, event)) { return if (event.isChannelOp(channel)) {
for (h in helpOp) { for (h in helpOp) {
event.sendMessage(helpCmdSyntax(h, event.bot().nick, true)) event.sendMessage(helpCmdSyntax(h, event.bot().nick, true))
} }
@ -141,7 +141,7 @@ class Ignore : AbstractCommand() {
override fun setProperty(key: String, value: String) { override fun setProperty(key: String, value: String) {
super.setProperty(key, value) super.setProperty(key, value)
if (IGNORE_PROP == key) { if (IGNORE_PROP == key) {
ignored.addAll(value.split(LinksMgr.TAG_MATCH.toRegex())) ignored.addAll(value.split(LinksManager.TAG_MATCH.toRegex()))
} }
} }

View file

@ -39,7 +39,7 @@ import net.thauvin.erik.mobibot.Utils.isChannelOp
import net.thauvin.erik.mobibot.Utils.plural import net.thauvin.erik.mobibot.Utils.plural
import net.thauvin.erik.mobibot.Utils.sendList import net.thauvin.erik.mobibot.Utils.sendList
import net.thauvin.erik.mobibot.Utils.sendMessage import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.links.LinksMgr import net.thauvin.erik.mobibot.commands.links.LinksManager
import net.thauvin.erik.mobibot.commands.seen.Seen import net.thauvin.erik.mobibot.commands.seen.Seen
import net.thauvin.erik.mobibot.commands.tell.Tell import net.thauvin.erik.mobibot.commands.tell.Tell
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
@ -107,16 +107,16 @@ class Info(private val tell: Tell, private val seen: Seen) : AbstractCommand() {
info.append("Uptime: ") info.append("Uptime: ")
.append(ManagementFactory.getRuntimeMXBean().uptime.toUptime()) .append(ManagementFactory.getRuntimeMXBean().uptime.toUptime())
.append(" [Entries: ") .append(" [Entries: ")
.append(LinksMgr.entries.links.size) .append(LinksManager.entries.links.size)
if (seen.isEnabled()) { if (seen.isEnabled()) {
info.append(", Seen: ").append(seen.count()) info.append(", Seen: ").append(seen.count())
} }
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
if (tell.isEnabled()) { if (tell.isEnabled()) {
info.append(", Messages: ").append(tell.size()) info.append(", Messages: ").append(tell.size())
} }
if (LinksMgr.twitter.isAutoPost) { if (LinksManager.socialManager.entriesCount() > 0) {
info.append(", Twitter: ").append(LinksMgr.twitter.entriesCount()) info.append(", Social: ").append(LinksManager.socialManager.entriesCount())
} }
} }
info.append(", Recap: ").append(Recap.recaps.size).append(']') info.append(", Recap: ").append(Recap.recaps.size).append(']')

View file

@ -45,7 +45,7 @@ class Me : AbstractCommand() {
override val isVisible = true override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
event.bot().sendIRC().action(channel, args) event.bot().sendIRC().action(channel, args)
} }
} }

View file

@ -45,7 +45,7 @@ class Modules(private val modules: List<String>) : AbstractCommand() {
override val isVisible = true override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
if (modules.isEmpty()) { if (modules.isEmpty()) {
event.respondPrivateMessage("There are no enabled modules.") event.respondPrivateMessage("There are no enabled modules.")
} else { } else {

View file

@ -48,7 +48,7 @@ class Msg : AbstractCommand() {
override val isVisible = true override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
val msg = args.split(" ", limit = 2) val msg = args.split(" ", limit = 2)
if (args.length > 2) { if (args.length > 2) {
event.bot().sendIRC().message(msg[0], msg[1]) event.bot().sendIRC().message(msg[0], msg[1])

View file

@ -45,7 +45,7 @@ class Nick : AbstractCommand() {
override val isVisible = true override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
event.bot().sendIRC().changeNick(args) event.bot().sendIRC().changeNick(args)
} }
} }

View file

@ -45,7 +45,7 @@ class Say : AbstractCommand() {
override val isVisible = true override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
event.bot().sendIRC().message(channel, args) event.bot().sendIRC().message(channel, args)
} }
} }

View file

@ -53,7 +53,7 @@ class Versions : AbstractCommand() {
override val isVisible = true override val isVisible = true
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
event.sendList(allVersions, 1) event.sendList(allVersions, 1)
} }
} }

View file

@ -66,8 +66,8 @@ class Comment : AbstractCommand() {
val cmds = args.substring(1).split("[.:]".toRegex(), 3) val cmds = args.substring(1).split("[.:]".toRegex(), 3)
val entryIndex = cmds[0].toInt() - 1 val entryIndex = cmds[0].toInt() - 1
if (entryIndex < LinksMgr.entries.links.size && LinksMgr.isUpToDate(event)) { if (entryIndex < LinksManager.entries.links.size && LinksManager.isUpToDate(event)) {
val entry: EntryLink = LinksMgr.entries.links[entryIndex] val entry: EntryLink = LinksManager.entries.links[entryIndex]
val commentIndex = cmds[1].toInt() - 1 val commentIndex = cmds[1].toInt() - 1
if (commentIndex < entry.comments.size) { if (commentIndex < entry.comments.size) {
when (val cmd = cmds[2].trim()) { when (val cmd = cmds[2].trim()) {
@ -87,7 +87,7 @@ class Comment : AbstractCommand() {
override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean { override fun helpResponse(channel: String, topic: String, event: GenericMessageEvent): Boolean {
if (super.helpResponse(channel, topic, event)) { if (super.helpResponse(channel, topic, event)) {
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
event.sendMessage("To change a comment's author:") event.sendMessage("To change a comment's author:")
event.sendMessage(helpFormat("${Constants.LINK_CMD}1.1:?<nick>")) event.sendMessage(helpFormat("${Constants.LINK_CMD}1.1:?<nick>"))
} }
@ -108,11 +108,11 @@ class Comment : AbstractCommand() {
commentIndex: Int, commentIndex: Int,
event: GenericMessageEvent event: GenericMessageEvent
) { ) {
if (isChannelOp(channel, event) && cmd.length > 1) { if (event.isChannelOp(channel) && cmd.length > 1) {
val comment = entry.getComment(commentIndex) val comment = entry.getComment(commentIndex)
comment.nick = cmd.substring(1) comment.nick = cmd.substring(1)
event.sendMessage(buildComment(entryIndex, commentIndex, comment)) event.sendMessage(buildComment(entryIndex, commentIndex, comment))
LinksMgr.entries.save() LinksManager.entries.save()
} else { } else {
event.sendMessage("Please ask a channel op to change the author of this comment for you.") event.sendMessage("Please ask a channel op to change the author of this comment for you.")
} }
@ -125,10 +125,10 @@ class Comment : AbstractCommand() {
commentIndex: Int, commentIndex: Int,
event: GenericMessageEvent event: GenericMessageEvent
) { ) {
if (isChannelOp(channel, event) || event.user.nick == entry.getComment(commentIndex).nick) { if (event.isChannelOp(channel) || event.user.nick == entry.getComment(commentIndex).nick) {
entry.deleteComment(commentIndex) entry.deleteComment(commentIndex)
event.sendMessage("Comment ${entryIndex.toLinkLabel()}.${commentIndex + 1} removed.") event.sendMessage("Comment ${entryIndex.toLinkLabel()}.${commentIndex + 1} removed.")
LinksMgr.entries.save() LinksManager.entries.save()
} else { } else {
event.sendMessage("Please ask a channel op to delete this comment for you.") event.sendMessage("Please ask a channel op to delete this comment for you.")
} }
@ -143,7 +143,7 @@ class Comment : AbstractCommand() {
) { ) {
entry.setComment(commentIndex, cmd, event.user.nick) entry.setComment(commentIndex, cmd, event.user.nick)
event.sendMessage(buildComment(entryIndex, commentIndex, entry.getComment(commentIndex))) event.sendMessage(buildComment(entryIndex, commentIndex, entry.getComment(commentIndex)))
LinksMgr.entries.save() LinksManager.entries.save()
} }
private fun showComment(entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent) { private fun showComment(entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent) {

View file

@ -1,5 +1,5 @@
/* /*
* LinksMgr.kt * LinksManager.kt
* *
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net) * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved. * All rights reserved.
@ -45,12 +45,12 @@ import net.thauvin.erik.mobibot.entries.Entries
import net.thauvin.erik.mobibot.entries.EntriesUtils.buildLink import net.thauvin.erik.mobibot.entries.EntriesUtils.buildLink
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import net.thauvin.erik.mobibot.entries.EntryLink import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.mobibot.modules.Twitter import net.thauvin.erik.mobibot.social.SocialManager
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.pircbotx.hooks.types.GenericMessageEvent import org.pircbotx.hooks.types.GenericMessageEvent
import java.io.IOException import java.io.IOException
class LinksMgr : AbstractCommand() { class LinksManager : AbstractCommand() {
private val defaultTags: MutableList<String> = mutableListOf() private val defaultTags: MutableList<String> = mutableListOf()
private val keywords: MutableList<String> = mutableListOf() private val keywords: MutableList<String> = mutableListOf()
@ -83,10 +83,10 @@ class LinksMgr : AbstractCommand() {
val pinboard = Pinboard() val pinboard = Pinboard()
/** /**
* Twitter handler. * Social Manager handler.
*/ */
@JvmField @JvmField
val twitter = Twitter() val socialManager = SocialManager()
/** /**
* Let the user know if the entries are too old to be modified. * Let the user know if the entries are too old to be modified.
@ -140,8 +140,8 @@ class LinksMgr : AbstractCommand() {
pinboard.addPin(event.bot().serverHostname, entry) pinboard.addPin(event.bot().serverHostname, entry)
// Queue link for posting to Twitter. // Queue link for posting to social media.
twitter.queueEntry(index) socialManager.queueEntry(index)
entries.save() entries.save()

View file

@ -39,7 +39,7 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.isChannelOp import net.thauvin.erik.mobibot.Utils.isChannelOp
import net.thauvin.erik.mobibot.Utils.sendMessage import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.AbstractCommand import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.commands.links.LinksMgr.Companion.entries import net.thauvin.erik.mobibot.commands.links.LinksManager.Companion.entries
import net.thauvin.erik.mobibot.entries.EntriesUtils import net.thauvin.erik.mobibot.entries.EntriesUtils
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import net.thauvin.erik.mobibot.entries.EntryLink import net.thauvin.erik.mobibot.entries.EntryLink
@ -71,7 +71,7 @@ class Posting : AbstractCommand() {
val cmd = cmds[1].trim() val cmd = cmds[1].trim()
if (cmd.isBlank()) { if (cmd.isBlank()) {
showEntry(entryIndex, event) // L1: showEntry(entryIndex, event) // L1:
} else if (LinksMgr.isUpToDate(event)) { } else if (LinksManager.isUpToDate(event)) {
if (cmd == "-") { if (cmd == "-") {
removeEntry(channel, entryIndex, event) // L1:- removeEntry(channel, entryIndex, event) // L1:-
} else { } else {
@ -102,7 +102,7 @@ class Posting : AbstractCommand() {
if (cmd.length > 1) { if (cmd.length > 1) {
val entry: EntryLink = entries.links[entryIndex] val entry: EntryLink = entries.links[entryIndex]
entry.title = cmd.substring(1).trim() entry.title = cmd.substring(1).trim()
LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry) LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
event.sendMessage(EntriesUtils.buildLink(entryIndex, entry)) event.sendMessage(EntriesUtils.buildLink(entryIndex, entry))
entries.save() entries.save()
} }
@ -110,12 +110,12 @@ class Posting : AbstractCommand() {
private fun changeUrl(channel: String, cmd: String, entryIndex: Int, event: GenericMessageEvent) { private fun changeUrl(channel: String, cmd: String, entryIndex: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries.links[entryIndex] val entry: EntryLink = entries.links[entryIndex]
if (entry.login == event.user.login || isChannelOp(channel, event)) { if (entry.login == event.user.login || event.isChannelOp(channel)) {
val link = cmd.substring(1) val link = cmd.substring(1)
if (link.matches(LinksMgr.LINK_MATCH.toRegex())) { if (link.matches(LinksManager.LINK_MATCH.toRegex())) {
val oldLink = entry.link val oldLink = entry.link
entry.link = link entry.link = link
LinksMgr.pinboard.updatePin(event.bot().serverHostname, oldLink, entry) LinksManager.pinboard.updatePin(event.bot().serverHostname, oldLink, entry)
event.sendMessage(EntriesUtils.buildLink(entryIndex, entry)) event.sendMessage(EntriesUtils.buildLink(entryIndex, entry))
entries.save() entries.save()
} }
@ -123,11 +123,11 @@ class Posting : AbstractCommand() {
} }
private fun changeAuthor(channel: String, cmd: String, index: Int, event: GenericMessageEvent) { private fun changeAuthor(channel: String, cmd: String, index: Int, event: GenericMessageEvent) {
if (isChannelOp(channel, event)) { if (event.isChannelOp(channel)) {
if (cmd.length > 1) { if (cmd.length > 1) {
val entry: EntryLink = entries.links[index] val entry: EntryLink = entries.links[index]
entry.nick = cmd.substring(1) entry.nick = cmd.substring(1)
LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry) LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
event.sendMessage(EntriesUtils.buildLink(index, entry)) event.sendMessage(EntriesUtils.buildLink(index, entry))
entries.save() entries.save()
} }
@ -138,9 +138,9 @@ class Posting : AbstractCommand() {
private fun removeEntry(channel: String, index: Int, event: GenericMessageEvent) { private fun removeEntry(channel: String, index: Int, event: GenericMessageEvent) {
val entry: EntryLink = entries.links[index] val entry: EntryLink = entries.links[index]
if (entry.login == event.user.login || isChannelOp(channel, event)) { if (entry.login == event.user.login || event.isChannelOp(channel)) {
LinksMgr.pinboard.deletePin(entry) LinksManager.pinboard.deletePin(entry)
LinksMgr.twitter.removeEntry(index) LinksManager.socialManager.removeEntry(index)
entries.links.removeAt(index) entries.links.removeAt(index)
event.sendMessage("Entry ${index.toLinkLabel()} removed.") event.sendMessage("Entry ${index.toLinkLabel()} removed.")
entries.save() entries.save()

View file

@ -60,15 +60,15 @@ class Tags : AbstractCommand() {
val cmds = args.substring(1).split("${Constants.TAG_CMD}:", limit = 2) val cmds = args.substring(1).split("${Constants.TAG_CMD}:", limit = 2)
val index = cmds[0].toInt() - 1 val index = cmds[0].toInt() - 1
if (index < LinksMgr.entries.links.size && LinksMgr.isUpToDate(event)) { if (index < LinksManager.entries.links.size && LinksManager.isUpToDate(event)) {
val cmd = cmds[1].trim() val cmd = cmds[1].trim()
val entry: EntryLink = LinksMgr.entries.links[index] val entry: EntryLink = LinksManager.entries.links[index]
if (cmd.isNotEmpty()) { if (cmd.isNotEmpty()) {
if (entry.login == event.user.login || isChannelOp(channel, event)) { if (entry.login == event.user.login || event.isChannelOp(channel)) {
entry.setTags(cmd) entry.setTags(cmd)
LinksMgr.pinboard.updatePin(event.bot().serverHostname, entry.link, entry) LinksManager.pinboard.updatePin(event.bot().serverHostname, entry.link, entry)
event.sendMessage(EntriesUtils.buildTags(index, entry)) event.sendMessage(EntriesUtils.buildTags(index, entry))
LinksMgr.entries.save() LinksManager.entries.save()
} else { } else {
event.sendMessage("Please ask a channel op to change the tags for you.") event.sendMessage("Please ask a channel op to change the tags for you.")
} }

View file

@ -38,7 +38,7 @@ import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.Utils.lastOrEmpty import net.thauvin.erik.mobibot.Utils.lastOrEmpty
import net.thauvin.erik.mobibot.Utils.sendMessage import net.thauvin.erik.mobibot.Utils.sendMessage
import net.thauvin.erik.mobibot.commands.AbstractCommand import net.thauvin.erik.mobibot.commands.AbstractCommand
import net.thauvin.erik.mobibot.commands.links.LinksMgr.Companion.entries import net.thauvin.erik.mobibot.commands.links.LinksManager.Companion.entries
import net.thauvin.erik.mobibot.entries.EntriesUtils import net.thauvin.erik.mobibot.entries.EntriesUtils
import net.thauvin.erik.mobibot.entries.EntryLink import net.thauvin.erik.mobibot.entries.EntryLink
import org.pircbotx.hooks.events.PrivateMessageEvent import org.pircbotx.hooks.events.PrivateMessageEvent

View file

@ -81,7 +81,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
* Cleans the messages queue. * Cleans the messages queue.
*/ */
private fun clean(): Boolean { private fun clean(): Boolean {
return TellMessagesMgr.clean(messages, maxDays.toLong()) return TellManager.clean(messages, maxDays.toLong())
} }
override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) { override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) {
@ -89,7 +89,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
if (args.isBlank()) { if (args.isBlank()) {
helpResponse(channel, args, event) helpResponse(channel, args, event)
} else if (args.startsWith(View.VIEW_CMD)) { } else if (args.startsWith(View.VIEW_CMD)) {
if (isChannelOp(channel, event) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) { if (event.isChannelOp(channel) && "${View.VIEW_CMD} $TELL_ALL_KEYWORD" == args) {
viewAll(event) viewAll(event)
} else { } else {
viewMessages(event) viewMessages(event)
@ -120,7 +120,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
} else { } else {
if (messages.removeIf { if (messages.removeIf {
it.id == id && it.id == id &&
(it.sender.equals(event.user.nick, true) || isChannelOp(channel, event)) (it.sender.equals(event.user.nick, true) || event.isChannelOp(channel))
}) { }) {
save() save()
event.sendMessage("The message was deleted from the queue.") event.sendMessage("The message was deleted from the queue.")
@ -167,7 +167,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
* Saves the messages queue. * Saves the messages queue.
*/ */
private fun save() { private fun save() {
TellMessagesMgr.save(serialObject, messages) TellManager.save(serialObject, messages)
} }
/** /**
@ -291,7 +291,7 @@ class Tell(private val serialObject: String) : AbstractCommand() {
initProperties(MAX_DAYS_PROP, MAX_SIZE_PROP) initProperties(MAX_DAYS_PROP, MAX_SIZE_PROP)
// Load the message queue // Load the message queue
messages.addAll(TellMessagesMgr.load(serialObject)) messages.addAll(TellManager.load(serialObject))
if (clean()) { if (clean()) {
save() save()
} }

View file

@ -1,5 +1,5 @@
/* /*
* TellMessagesMgr.kt * TellManager.kt
* *
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net) * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved. * All rights reserved.
@ -41,8 +41,8 @@ import java.time.LocalDateTime
/** /**
* The Tell Messages Manager. * The Tell Messages Manager.
*/ */
object TellMessagesMgr { object TellManager {
private val logger: Logger = LoggerFactory.getLogger(TellMessagesMgr::class.java) private val logger: Logger = LoggerFactory.getLogger(TellManager::class.java)
/** /**
* Cleans the messages queue. * Cleans the messages queue.

View file

@ -45,11 +45,11 @@ class Entries(
var lastPubDate = today() var lastPubDate = today()
fun load() { fun load() {
lastPubDate = FeedsMgr.loadFeed(this) lastPubDate = FeedsManager.loadFeed(this)
} }
fun save() { fun save() {
lastPubDate = today() lastPubDate = today()
FeedsMgr.saveFeed(this) FeedsManager.saveFeed(this)
} }
} }

View file

@ -33,7 +33,7 @@ package net.thauvin.erik.mobibot.entries
import com.rometools.rome.feed.synd.SyndCategory import com.rometools.rome.feed.synd.SyndCategory
import com.rometools.rome.feed.synd.SyndCategoryImpl import com.rometools.rome.feed.synd.SyndCategoryImpl
import net.thauvin.erik.mobibot.commands.links.LinksMgr import net.thauvin.erik.mobibot.commands.links.LinksManager
import java.io.Serializable import java.io.Serializable
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
@ -169,7 +169,7 @@ class EntryLink(
* Sets the tags. * Sets the tags.
*/ */
fun setTags(tags: String) { fun setTags(tags: String) {
setTags(tags.split(LinksMgr.TAG_MATCH)) setTags(tags.split(LinksManager.TAG_MATCH))
} }
/** /**

View file

@ -1,5 +1,5 @@
/* /*
* FeedsMgr.kt * FeedsManager.kt
* *
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net) * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved. * All rights reserved.
@ -55,9 +55,9 @@ import kotlin.io.path.exists
/** /**
* Manages the RSS feeds. * Manages the RSS feeds.
*/ */
class FeedsMgr private constructor() { class FeedsManager private constructor() {
companion object { companion object {
private val logger: Logger = LoggerFactory.getLogger(FeedsMgr::class.java) private val logger: Logger = LoggerFactory.getLogger(FeedsManager::class.java)
// The file containing the current entries. // The file containing the current entries.
private const val currentXml = "current.xml" private const val currentXml = "current.xml"

View file

@ -0,0 +1,150 @@
/*
* Mastodon.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.modules
import net.thauvin.erik.mobibot.Utils
import net.thauvin.erik.mobibot.Utils.prefixIfMissing
import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.mobibot.social.SocialModule
import org.json.JSONException
import org.json.JSONObject
import org.json.JSONWriter
import java.io.IOException
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
class Mastodon : SocialModule() {
override val name = "Mastodon"
override val handle: String?
get() = properties[HANDLE_PROP]
override val isAutoPost: Boolean
get() = isEnabled && properties[Twitter.AUTO_POST_PROP].toBoolean()
override val isValidProperties: Boolean
get() {
for (s in propertyKeys) {
if (AUTO_POST_PROP != s && HANDLE_PROP != s && properties[s].isNullOrBlank()) {
return false
}
}
return true
}
/**
* Formats the entry for posting.
*/
override fun formatEntry(entry: EntryLink): String {
return "${entry.title} via ${entry.nick} on ${entry.channel}\n\n${entry.link}"
}
/**
* Posts on Mastodon.
*/
@Throws(ModuleException::class)
override fun post(message: String, isDm: Boolean): String {
return toot(
apiKey = properties[ACCESS_TOKEN_PROP],
instance = properties[INSTANCE_PROP],
handle = handle,
message = message,
isDm = isDm
)
}
companion object {
// Property keys
const val ACCESS_TOKEN_PROP = "mastodon-access-token"
const val AUTO_POST_PROP = "mastodon-auto-post"
const val HANDLE_PROP = "mastodon-handle"
const val INSTANCE_PROP = "mastodon-instance"
private const val MASTODON_CMD = "mastodon"
/**
* Toots on Mastodon.
*/
@JvmStatic
@Throws(ModuleException::class)
fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String {
val request = HttpRequest.newBuilder()
.uri(URI.create("https://$instance/api/v1/statuses"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer $apiKey")
.POST(
HttpRequest.BodyPublishers.ofString(
JSONWriter.valueToString(
if (isDm) {
mapOf("status" to "${handle?.prefixIfMissing('@')} $message", "visibility" to "direct")
} else {
mapOf("status" to message)
}
)
)
)
.build()
try {
val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() == 200) {
return try {
val jsonResponse = JSONObject(response.body())
if (isDm) {
jsonResponse.getString("content")
} else {
"Your message was posted to ${jsonResponse.getString("url")}"
}
} catch (e: JSONException) {
throw ModuleException("mastodonPost($message)", "A JSON error has occurred: ${e.message}", e)
}
} else {
throw IOException("Status Code: " + response.statusCode())
}
} catch (e: IOException) {
throw ModuleException("mastodonPost($message)", "An IO error has occurred: ${e.message}", e)
} catch (e: InterruptedException) {
throw ModuleException("mastodonPost($message)", "An error has occurred: ${e.message}", e)
}
}
}
init {
commands.add(MASTODON_CMD)
help.add("To post to Mastodon:")
help.add(Utils.helpFormat("%c $MASTODON_CMD <message>"))
properties[AUTO_POST_PROP] = "false"
initProperties(ACCESS_TOKEN_PROP, HANDLE_PROP, INSTANCE_PROP)
}
}

View file

@ -31,57 +31,27 @@
*/ */
package net.thauvin.erik.mobibot.modules package net.thauvin.erik.mobibot.modules
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.TwitterTimer
import net.thauvin.erik.mobibot.Utils.helpFormat import net.thauvin.erik.mobibot.Utils.helpFormat
import net.thauvin.erik.mobibot.commands.links.LinksMgr import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel import net.thauvin.erik.mobibot.social.SocialModule
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import twitter4j.TwitterException import twitter4j.TwitterException
import java.util.Timer
/** /**
* The Twitter module. * The Twitter module.
*/ */
class Twitter : ThreadedModule() { class Twitter : SocialModule() {
private val logger: Logger = LoggerFactory.getLogger(Twitter::class.java)
private val timer = Timer(true)
// Twitter auto-posts.
private val entries: MutableSet<Int> = HashSet()
override val name = "Twitter" override val name = "Twitter"
/** override val handle: String?
* Add an entry to be posted on Twitter.
*/
private fun addEntry(index: Int) {
entries.add(index)
}
fun entriesCount(): Int {
return entries.size
}
private val handle: String?
get() = properties[HANDLE_PROP] get() = properties[HANDLE_PROP]
private fun hasEntry(index: Int): Boolean { override val isAutoPost: Boolean
return entries.contains(index) get() = isEnabled && properties[AUTO_POST_PROP].toBoolean()
}
val isAutoPost: Boolean
get() = isEnabled && properties[AUTOPOST_PROP].toBoolean()
override val isValidProperties: Boolean override val isValidProperties: Boolean
get() { get() {
for (s in propertyKeys) { for (s in propertyKeys) {
if (AUTOPOST_PROP != s && HANDLE_PROP != s && properties[s].isNullOrBlank()) { if (AUTO_POST_PROP != s && HANDLE_PROP != s && properties[s].isNullOrBlank()) {
return false return false
} }
} }
@ -89,105 +59,31 @@ class Twitter : ThreadedModule() {
} }
/** /**
* Send a notification to the registered Twitter handle. * Formats the entry for posting.
*/ */
fun notification(msg: String) { override fun formatEntry(entry: EntryLink): String {
if (isEnabled && !handle.isNullOrBlank()) { return "${entry.title} ${entry.link} via ${entry.nick} on ${entry.channel}"
runBlocking {
launch {
try {
post(message = msg, isDm = true)
if (logger.isDebugEnabled) logger.debug("Notified @$handle: $msg")
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn("Failed to notify @$handle: $msg", e)
}
}
}
}
} }
/** /**
* Posts on Twitter. * Posts on Twitter.
*/ */
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun post(handle: String = "${properties[HANDLE_PROP]}", message: String, isDm: Boolean): String { override fun post(message: String, isDm: Boolean): String {
return twitterPost( return tweet(
properties[CONSUMER_KEY_PROP], consumerKey = properties[CONSUMER_KEY_PROP],
properties[CONSUMER_SECRET_PROP], consumerSecret = properties[CONSUMER_SECRET_PROP],
properties[TOKEN_PROP], token = properties[TOKEN_PROP],
properties[TOKEN_SECRET_PROP], tokenSecret = properties[TOKEN_SECRET_PROP],
handle, handle = handle,
message, message = message,
isDm isDm = isDm
) )
} }
/**
* Post an entry to twitter.
*/
fun postEntry(index: Int) {
if (isAutoPost && hasEntry(index) && LinksMgr.entries.links.size >= index) {
val entry = LinksMgr.entries.links[index]
val msg = "${entry.title} ${entry.link} via ${entry.nick} on ${entry.channel}"
runBlocking {
launch {
try {
if (logger.isDebugEnabled) {
logger.debug("Posting {} to Twitter.", index.toLinkLabel())
}
post(message = msg, isDm = false)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn("Failed to post entry on Twitter.", e)
}
}
}
removeEntry(index)
}
}
fun queueEntry(index: Int) {
if (isAutoPost) {
addEntry(index)
if (logger.isDebugEnabled) {
logger.debug("Scheduling {} for posting on Twitter.", index.toLinkLabel())
}
timer.schedule(TwitterTimer(this, index), Constants.TIMER_DELAY * 60L * 1000L)
}
}
fun removeEntry(index: Int) {
entries.remove(index)
}
/**
* Posts to twitter.
*/
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
try {
event.respond(post(event.user.nick, "$args (by ${event.user.nick} on $channel)", false))
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let {
event.respond(it)
}
}
}
/**
* Post all the entries to Twitter on shutdown.
*/
fun shutdown() {
timer.cancel()
if (isAutoPost) {
for (index in entries) {
postEntry(index)
}
}
}
companion object { companion object {
// Property keys // Property keys
const val AUTOPOST_PROP = "twitter-auto-post" const val AUTO_POST_PROP = "twitter-auto-post"
const val CONSUMER_KEY_PROP = "twitter-consumerKey" const val CONSUMER_KEY_PROP = "twitter-consumerKey"
const val CONSUMER_SECRET_PROP = "twitter-consumerSecret" const val CONSUMER_SECRET_PROP = "twitter-consumerSecret"
const val HANDLE_PROP = "twitter-handle" const val HANDLE_PROP = "twitter-handle"
@ -198,11 +94,11 @@ class Twitter : ThreadedModule() {
private const val TWITTER_CMD = "twitter" private const val TWITTER_CMD = "twitter"
/** /**
* Posts on Twitter. * Tweets on Twitter.
*/ */
@JvmStatic @JvmStatic
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun twitterPost( fun tweet(
consumerKey: String?, consumerKey: String?,
consumerSecret: String?, consumerSecret: String?,
token: String?, token: String?,
@ -236,7 +132,7 @@ class Twitter : ThreadedModule() {
commands.add(TWITTER_CMD) commands.add(TWITTER_CMD)
help.add("To post to Twitter:") help.add("To post to Twitter:")
help.add(helpFormat("%c $TWITTER_CMD <message>")) help.add(helpFormat("%c $TWITTER_CMD <message>"))
properties[AUTOPOST_PROP] = "false" properties[AUTO_POST_PROP] = "false"
initProperties(CONSUMER_KEY_PROP, CONSUMER_SECRET_PROP, HANDLE_PROP, TOKEN_PROP, TOKEN_SECRET_PROP) initProperties(CONSUMER_KEY_PROP, CONSUMER_SECRET_PROP, HANDLE_PROP, TOKEN_PROP, TOKEN_SECRET_PROP)
} }
} }

View file

@ -0,0 +1,117 @@
/*
* SocialManager.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.social
import net.thauvin.erik.mobibot.Addons
import net.thauvin.erik.mobibot.Constants
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.Timer
/**
* Social Manager.
*/
class SocialManager {
private val entries: MutableSet<Int> = HashSet()
private val logger: Logger = LoggerFactory.getLogger(SocialManager::class.java)
private val modules = ArrayList<SocialModule>()
private val timer = Timer(true)
/**
* Adds social modules.
*/
fun add(addons: Addons, vararg modules: SocialModule) {
modules.forEach {
if (addons.add(it)) {
this.modules.add(it)
}
}
}
/**
* Returns the number of entries.
*/
fun entriesCount(): Int = entries.size
/**
* Sends a social notification (dm, etc.)
*/
fun notification(msg: String) {
modules.forEach {
it.notification(msg)
}
}
/**
* Posts to social media.
*/
fun postEntry(index: Int) {
if (entries.contains(index)) {
modules.forEach {
it.postEntry(index)
}
entries.remove(index)
}
}
/**
* Queues an entry for posting to social media.
*/
fun queueEntry(index: Int) {
if (modules.isNotEmpty()) {
entries.add(index)
if (logger.isDebugEnabled) {
logger.debug("Scheduling {} for posting on social media.", index.toLinkLabel())
}
timer.schedule(SocialTimer(this, index), Constants.TIMER_DELAY * 60L * 1000L)
}
}
/**
* Removes entries from queue.
*/
fun removeEntry(index: Int) {
entries.remove(index)
}
/**
* Posts all entries on shutdown.
*/
fun shutdown() {
timer.cancel()
for (index in entries) {
postEntry(index)
}
}
}

View file

@ -0,0 +1,107 @@
/*
* SocialModule.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.social
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.thauvin.erik.mobibot.commands.links.LinksManager
import net.thauvin.erik.mobibot.entries.EntriesUtils.toLinkLabel
import net.thauvin.erik.mobibot.entries.EntryLink
import net.thauvin.erik.mobibot.modules.ModuleException
import net.thauvin.erik.mobibot.modules.ThreadedModule
import org.pircbotx.hooks.types.GenericMessageEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
abstract class SocialModule : ThreadedModule() {
private val logger: Logger = LoggerFactory.getLogger(SocialManager::class.java)
abstract val handle: String?
abstract val isAutoPost: Boolean
abstract fun formatEntry(entry: EntryLink): String
/**
* Sends a DM.
*/
fun notification(msg: String) {
if (isEnabled && !handle.isNullOrBlank()) {
runBlocking {
launch {
try {
post(message = msg, isDm = true)
if (logger.isDebugEnabled) logger.debug("Notified $handle on $name: $msg")
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn("Failed to notify $handle on $name: $msg", e)
}
}
}
}
}
abstract fun post(message: String, isDm: Boolean): String
/**
* Post entry to social media.
*/
fun postEntry(index: Int) {
if (isAutoPost && LinksManager.entries.links.size >= index) {
runBlocking {
launch {
try {
if (logger.isDebugEnabled) {
logger.debug("Posting {} to $name.", index.toLinkLabel())
}
post(message = formatEntry(LinksManager.entries.links[index]), isDm = false)
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(
"Failed to post entry ${index.toLinkLabel()} on $name.",
e
)
}
}
}
}
}
override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) {
try {
event.respond(post("$args (by ${event.user.nick} on $channel)", false))
} catch (e: ModuleException) {
if (logger.isWarnEnabled) logger.warn(e.debugMessage, e)
e.message?.let {
event.respond(it)
}
}
}
}

View file

@ -1,5 +1,5 @@
/* /*
* TwitterTimer.kt * SocialTimer.kt
* *
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net) * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved. * All rights reserved.
@ -30,13 +30,12 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package net.thauvin.erik.mobibot package net.thauvin.erik.mobibot.social
import net.thauvin.erik.mobibot.modules.Twitter
import java.util.TimerTask import java.util.TimerTask
class TwitterTimer(private var twitter: Twitter, private var index: Int) : TimerTask() { class SocialTimer(private var socialManager: SocialManager, private var index: Int) : TimerTask() {
override fun run() { override fun run() {
twitter.postEntry(index) socialManager.postEntry(index)
} }
} }

View file

@ -33,6 +33,8 @@ package net.thauvin.erik.mobibot
import org.testng.annotations.BeforeSuite import org.testng.annotations.BeforeSuite
import java.io.IOException import java.io.IOException
import java.net.InetAddress
import java.net.UnknownHostException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import java.util.Properties import java.util.Properties
@ -56,6 +58,15 @@ open class LocalProperties {
companion object { companion object {
private val localProps = Properties() private val localProps = Properties()
fun getHostName(): String {
val ciName = System.getenv("CI_NAME")
return ciName ?: try {
InetAddress.getLocalHost().hostName
} catch (ignore: UnknownHostException) {
"Unknown Host"
}
}
fun getProperty(key: String): String { fun getProperty(key: String): String {
return if (localProps.containsKey(key)) { return if (localProps.containsKey(key)) {
localProps.getProperty(key) localProps.getProperty(key)

View file

@ -41,31 +41,34 @@ import assertk.assertions.size
import net.thauvin.erik.mobibot.Constants import net.thauvin.erik.mobibot.Constants
import org.testng.annotations.Test import org.testng.annotations.Test
class LinksMgrTest { class LinksManagerTest {
private val linksMgr = LinksMgr() private val linksManager = LinksManager()
@Test(groups = ["commands", "links"]) @Test(groups = ["commands", "links"])
fun fetchTitle() { fun fetchTitle() {
assertThat(linksMgr.fetchTitle("https://erik.thauvin.net/"), "fetchTitle(Erik)").contains("Erik's Weblog") assertThat(linksManager.fetchTitle("https://erik.thauvin.net/"), "fetchTitle(Erik)").contains("Erik's Weblog")
assertThat(linksMgr.fetchTitle("https://www.google.com/foo"), "fetchTitle(Foo)").isEqualTo(Constants.NO_TITLE) assertThat(
linksManager.fetchTitle("https://www.google.com/foo"),
"fetchTitle(Foo)"
).isEqualTo(Constants.NO_TITLE)
} }
@Test(groups = ["commands", "links"]) @Test(groups = ["commands", "links"])
fun testMatches() { fun testMatches() {
assertThat(linksMgr.matches("https://www.example.com/"), "matches(url)").isTrue() assertThat(linksManager.matches("https://www.example.com/"), "matches(url)").isTrue()
assertThat(linksMgr.matches("HTTP://erik.thauvin.net/blog/ Erik's Weblog"), "matches(HTTP)").isTrue() assertThat(linksManager.matches("HTTP://erik.thauvin.net/blog/ Erik's Weblog"), "matches(HTTP)").isTrue()
} }
@Test(groups = ["commands", "links"]) @Test(groups = ["commands", "links"])
fun matchTagKeywordsTest() { fun matchTagKeywordsTest() {
linksMgr.setProperty(LinksMgr.KEYWORDS_PROP, "key1 key2,key3") linksManager.setProperty(LinksManager.KEYWORDS_PROP, "key1 key2,key3")
val tags = mutableListOf<String>() val tags = mutableListOf<String>()
linksMgr.matchTagKeywords("Test title with key2", tags) linksManager.matchTagKeywords("Test title with key2", tags)
assertThat(tags, "tags").contains("key2") assertThat(tags, "tags").contains("key2")
tags.clear() tags.clear()
linksMgr.matchTagKeywords("Test key3 title with key1", tags) linksManager.matchTagKeywords("Test key3 title with key1", tags)
assertThat(tags, "tags(key1, key3)").all { assertThat(tags, "tags(key1, key3)").all {
contains("key1") contains("key1")
contains("key3") contains("key3")

View file

@ -45,7 +45,7 @@ class ViewTest {
val view = View() val view = View()
for (i in 1..10) { for (i in 1..10) {
LinksMgr.entries.links.add( LinksManager.entries.links.add(
EntryLink( EntryLink(
"https://www.example.com/$i", "https://www.example.com/$i",
"Example $i", "Example $i",
@ -98,11 +98,11 @@ class ViewTest {
} }
assertThat(view.parseArgs(""), "parseArgs()").all { assertThat(view.parseArgs(""), "parseArgs()").all {
prop(Pair<Int, String>::first).isEqualTo(LinksMgr.entries.links.size - View.MAX_ENTRIES) prop(Pair<Int, String>::first).isEqualTo(LinksManager.entries.links.size - View.MAX_ENTRIES)
prop(Pair<Int, String>::second).isEqualTo("") prop(Pair<Int, String>::second).isEqualTo("")
} }
LinksMgr.entries.links.clear() LinksManager.entries.links.clear()
assertThat(view.parseArgs("4"), "parseArgs(4)").all { assertThat(view.parseArgs("4"), "parseArgs(4)").all {
prop(Pair<Int, String>::first).isEqualTo(0) prop(Pair<Int, String>::first).isEqualTo(0)

View file

@ -60,7 +60,7 @@ class TellMessagesMgrTest {
@BeforeClass @BeforeClass
fun saveTest() { fun saveTest() {
TellMessagesMgr.save(testFile.toAbsolutePath().toString(), testMessages) TellManager.save(testFile.toAbsolutePath().toString(), testMessages)
assertThat(testFile.fileSize()).isGreaterThan(0) assertThat(testFile.fileSize()).isGreaterThan(0)
} }
@ -75,14 +75,14 @@ class TellMessagesMgrTest {
queued = LocalDateTime.now().minusDays(maxDays) queued = LocalDateTime.now().minusDays(maxDays)
}) })
val size = testMessages.size val size = testMessages.size
assertThat(TellMessagesMgr.clean(testMessages, maxDays + 2), "clean(maxDays=${maxDays + 2})").isFalse() assertThat(TellManager.clean(testMessages, maxDays + 2), "clean(maxDays=${maxDays + 2})").isFalse()
assertThat(TellMessagesMgr.clean(testMessages, maxDays), "clean(maxDays=$maxDays)").isTrue() assertThat(TellManager.clean(testMessages, maxDays), "clean(maxDays=$maxDays)").isTrue()
assertThat(testMessages, "testMessages").size().isEqualTo(size - 1) assertThat(testMessages, "testMessages").size().isEqualTo(size - 1)
} }
@Test(groups = ["commands", "tell"]) @Test(groups = ["commands", "tell"])
fun loadTest() { fun loadTest() {
val messages = TellMessagesMgr.load(testFile.toAbsolutePath().toString()) val messages = TellManager.load(testFile.toAbsolutePath().toString())
for (i in messages.indices) { for (i in messages.indices) {
assertThat(messages).index(i).all { assertThat(messages).index(i).all {
prop(TellMessage::sender).isEqualTo(testMessages[i].sender) prop(TellMessage::sender).isEqualTo(testMessages[i].sender)

View file

@ -65,7 +65,7 @@ class FeedMgrTest {
@Test(groups = ["entries"]) @Test(groups = ["entries"])
fun testFeedMgr() { fun testFeedMgr() {
// Load the feed // Load the feed
assertThat(FeedsMgr.loadFeed(entries), "loadFeed()").isEqualTo("2021-10-31") assertThat(FeedsManager.loadFeed(entries), "loadFeed()").isEqualTo("2021-10-31")
assertThat(entries.links, "entries.links").size().isEqualTo(2) assertThat(entries.links, "entries.links").size().isEqualTo(2)
entries.links.forEachIndexed { i, entryLink -> entries.links.forEachIndexed { i, entryLink ->
@ -101,7 +101,7 @@ class FeedMgrTest {
val backlogFile = Paths.get("${entries.logsDir}${today()}.xml") val backlogFile = Paths.get("${entries.logsDir}${today()}.xml")
// Save the feed // Save the feed
FeedsMgr.saveFeed(entries, currentFile.name) FeedsManager.saveFeed(entries, currentFile.name)
assertThat(currentFile, "currentFile").exists() assertThat(currentFile, "currentFile").exists()
assertThat(backlogFile, "backlogFile").exists() assertThat(backlogFile, "backlogFile").exists()
@ -110,7 +110,7 @@ class FeedMgrTest {
// Load the test feed // Load the test feed
entries.links.clear() entries.links.clear()
FeedsMgr.loadFeed(entries, currentFile.name) FeedsManager.loadFeed(entries, currentFile.name)
entries.links.forEachIndexed { i, entryLink -> entries.links.forEachIndexed { i, entryLink ->
assertThat(entryLink.title, "entryLink.title[${i + 1}]").isEqualTo("Example ${i + 1}") assertThat(entryLink.title, "entryLink.title[${i + 1}]").isEqualTo("Example ${i + 1}")

View file

@ -0,0 +1,56 @@
/*
* MastodonTest.kt
*
* Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of this project nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.thauvin.erik.mobibot.modules
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.isSuccess
import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.Mastodon.Companion.toot
import org.testng.annotations.Test
class MastodonTest : LocalProperties() {
@Test(groups = ["modules"])
@Throws(ModuleException::class)
fun testToot() {
val msg = "Testing Mastodon API from ${getHostName()}"
assertThat {
toot(
getProperty(Mastodon.ACCESS_TOKEN_PROP),
getProperty(Mastodon.INSTANCE_PROP),
getProperty(Mastodon.HANDLE_PROP),
msg,
true
)
}.isSuccess().contains(msg)
}
}

View file

@ -35,31 +35,19 @@ import assertk.assertThat
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import assertk.assertions.isSuccess import assertk.assertions.isSuccess
import net.thauvin.erik.mobibot.LocalProperties import net.thauvin.erik.mobibot.LocalProperties
import net.thauvin.erik.mobibot.modules.Twitter.Companion.twitterPost import net.thauvin.erik.mobibot.modules.Twitter.Companion.tweet
import org.testng.annotations.Test import org.testng.annotations.Test
import java.net.InetAddress
import java.net.UnknownHostException
/** /**
* The `TwitterTest` class. * The `TwitterTest` class.
*/ */
class TwitterTest : LocalProperties() { class TwitterTest : LocalProperties() {
private val ci: String
get() {
val ciName = System.getenv("CI_NAME")
return ciName ?: try {
InetAddress.getLocalHost().hostName
} catch (ignore: UnknownHostException) {
"Unknown Host"
}
}
@Test(groups = ["modules"]) @Test(groups = ["modules"])
@Throws(ModuleException::class) @Throws(ModuleException::class)
fun testPostTwitter() { fun testTweet() {
val msg = "Testing Twitter API from $ci" val msg = "Testing Twitter API from ${getHostName()}"
assertThat { assertThat {
twitterPost( tweet(
getProperty(Twitter.CONSUMER_KEY_PROP), getProperty(Twitter.CONSUMER_KEY_PROP),
getProperty(Twitter.CONSUMER_SECRET_PROP), getProperty(Twitter.CONSUMER_SECRET_PROP),
getProperty(Twitter.TOKEN_PROP), getProperty(Twitter.TOKEN_PROP),

View file

@ -1,9 +1,9 @@
#Generated by the Semver Plugin for Gradle #Generated by the Semver Plugin for Gradle
#Sun Dec 04 17:34:43 PST 2022 #Mon Dec 05 21:57:24 PST 2022
version.buildmeta=799 version.buildmeta=814
version.major=0 version.major=0
version.minor=8 version.minor=8
version.patch=0 version.patch=0
version.prerelease=rc version.prerelease=rc
version.project=mobibot version.project=mobibot
version.semver=0.8.0-rc+799 version.semver=0.8.0-rc+814

View file

@ -81,9 +81,9 @@
<li>Performing Google searches <li>Performing Google searches
<div><code>mobibot: google mobitopia on irc</code></div> <div><code>mobibot: google mobitopia on irc</code></div>
</li> </li>
<li>Getting answers from <a href="https://wolframalpha.com/">Wolfram Alpha</a> <li>Getting answers from <a href="https://wolframalpha.com/">Wolfram Alpha</a> and <a href="https://chat.openai.com/chat">ChatGPT</a>
<div><code>mobibot: wolfram days until christmas</code></div> <div><code>mobibot: wolfram days until christmas</code></div>
<div><code>mobibot: wolfram 1 gallon to liter</code></div> <div><code>mobibot: chatgpt explain quantum computing in simple terms</code></div>
</li> </li>
<li>Displaying weather information <li>Displaying weather information
<div><code>mobibot: weather san francisco</code></div> <div><code>mobibot: weather san francisco</code></div>
@ -116,13 +116,13 @@
<li>Random jokes from <a href="https://v2.jokeapi.dev/">Sv443's JokeAPI</a> <li>Random jokes from <a href="https://v2.jokeapi.dev/">Sv443's JokeAPI</a>
<div><code>mobibot: joke</code></div> <div><code>mobibot: joke</code></div>
</li> </li>
<li>Rolling dice or Playing war and rock paper scissors <li>Playing dice, war or rock paper scissors
<div><code>mobibot: dice</code></div> <div><code>mobibot: dice</code></div>
<div><code>mobibot: war</code></div> <div><code>mobibot: war</code></div>
<div><code>mobibot: paper</code></div> <div><code>mobibot: paper</code></div>
<div><code>mobibot: rock</code></div> <div><code>mobibot: rock</code></div>
</li> </li>
<li>Posting to <a href="https://twitter.com/mobitopia">Twitter</a></li> <li>Posting to <a href="https://twitter.com/mobitopia">Twitter</a> and <a href="https://joinmastodon.org/">Mastodon</a></li>
</ul> </ul>
<p>Some of the internal features include RSS feed backlogs, rolling logs, debugging toggle and much more.</p> <p>Some of the internal features include RSS feed backlogs, rolling logs, debugging toggle and much more.</p>